百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

夜读源码发现Android源码Bug(android源码阅读工具)

toyiye 2024-07-15 01:38 9 浏览 0 评论

前言

LruCache 是我们经常使用的缓存机制,也叫 “最近最少使用的” 缓存策略。其本质原理是通过历史访问记录来 倒序淘汰数据,它认为刚刚访问的数据,将来被访问的可能性较大,因此将该类数据维护到相对安全的区域,防止被淘汰。此时如果超过设定的内存瓶颈,将优先淘汰最老的数据。

  • 比如你在玩一款游戏,游戏里有一个仓库,有 20 个位置可以存放装备。
  • 1-20 件装备都可以正常存放,并根据时间顺序进行了排序,最晚存放的装备排在最后。
  • 当你使用其中任意一件装备的时候,这件装备的排序就变成了最后。
  • 有一天你又获得了一件装备,但是你的仓库已经满了,只能扔一件才能存储刚刚获得的,扔的顺序是从头开始扔。新装备存储后,排序为最后。依次类推。

1. 分析源码

接下来我们通过正常的使用来分析源码的逻辑,以下所有源码均来自 API 28。

通过 LruCache 提供的每一个方法进行一对一分析,首先我们看一段正常使用 LruCache 的代码

private void lruTest() {
        LruCache<Integer, Integer> lruCache = new LruCache<>(5);

        lruCache.put(1, 1);
        lruCache.put(2, 2);
        lruCache.put(3, 3);
        lruCache.put(4, 4);
        lruCache.put(5, 5);

        for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
            Log.e("LRU", entry.getKey() + ":" + entry.getValue());
        }

        Log.e("LRU", "超出设定存储容量后");

        lruCache.put(6, 6);

        for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
            Log.e("LRU", entry.getKey() + ":" + entry.getValue());
        }
    }

---日志输出---

2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 1:1
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 2:2
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 3:3
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 4:4
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 5:5
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 超出设定存储容量后
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 2:2
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 3:3
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 4:4
2019-11-18 18:21:03.625 24293-24293/com.we.we E/LRU: 5:5
2019-11-18 18:21:03.625 24293-24293/com.we.we E/LRU: 6:6

以上的例子的流程比较简单,初始化一个容量为 5 的 LruCache。

调用 put 方法添加数据,最后通过 snapshot () 方法获取 Map 然后遍历打印。

但是超出设定容量后再进行 put 数据,会将最先 put 的数据顶替掉,我们记住这个特性往下看。

2. LruCache 构造函数

如果有特殊需求可以通过重写 sizeOf()方法设定每条缓存数据的大小

  public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;

        //初始化一个负载因子为0.75,启动访问顺序排序的LinkedHashMap对象。至于LinkedHashMap是什么东西,我们后续分析。
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

put () 方法源码解析

    /**
     * 对于 key,缓存其相应的 value,key-value 条目放置于队尾
     *
     * @return 返回先前 key 对应的 value 值
     */

    public final V put(K key, V value) {
        //如果键值有一个为空,则抛空指针异常。
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;

        synchronized (this) {
            putCount++;
            //先给 size +1, safeSizeOf()方法返回值默认是1
            size += safeSizeOf(key, value);
            //map.put()方法返被替换的值,如果没有被替换,则返回null。(只有当前key在put之前就已经有数据,这种情况存储才会被替换)
            previous = map.put(key, value);
            if (previous != null) {
                //如果是被顶替的数据,那size需要-1,因为没有新增数据。
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            //当多线程中一个线程调用到了create方法,另一个线程调用到了put,此时一个key可能生成了两个值,因此需要通过重写这个方法进行处理。该方法默认为空方法。
            entryRemoved(false, key, previous, value);
        }

        //判断是否超出设定容量,如果超出,则循环删除最早数据,直到容量<=设定值。
        trimToSize(maxSize);
        return previous;
    }

3. put () 方法小结

  1. 首先会进行 K/V 的 null 判断,如果为 null 则抛出异常。
  2. 加锁并自增元素条目计数器,通过 LinkedHashMap.put 方法将 K/V 添加到 LinkedHashMap 对象中,如果 put 方法返回值不为 null,则说明原来该 key 下有数据,因此只是更新 key 对应的数据并没有新增数据,所以此时 size 需 -1。
  3. 如果 previous!=null,说明当前的 put 操作替换了之前的一个值,如果重写了 entryRemoved () 方法,则会回调到该方法中进行处理,如果没重写 entryRemoved (),该方法默认不做任何处理。
  4. 调用 trimToSize (maxSize) 进行容量检测和处理。

4. trimToSize () 方法源码解析,以及 bug 发现

  /**
   *删除最旧的元素,直到剩余数据总数 <= maxSize。
   */
private void trimToSize(int maxSize) {
  while (true) {
  K key;
  V value;

  synchronized (this) {
    if (size < 0 || (map.isEmpty() && size != 0)) {
      throw new IllegalStateException(getClass().getName()
           + ".sizeOf() is reporting inconsistent results!");
     }

   //如果当前元素数<=设定的最大值,则直接break退出循环。
   if (size <= maxSize) {
      break;
   }

  // BEGIN LAYOUTLIB CHANGE
  // get the last item in the linked list.
  // This is not efficient, the goal here is to minimize the changes
  // compared to the platform version.

  Map.Entry<K, V> toEvict = null;

  for (Map.Entry<K, V> entry : map.entrySet()) {
    //获取最少使用的数据元素,此处的写法是源码的一个bug,后续我会详细说明。
     toEvict = entry;
  }
  // END LAYOUTLIB CHANGE

  if (toEvict == null) {
    break;
  }

  key = toEvict.getKey();
  value = toEvict.getValue();

    //移除元素
    map.remove(key);
    //计数长度size -1;
    size -= safeSizeOf(key, value);
    //缓存移除的次数 +1
      evictionCount++;
     }
    entryRemoved(true, key, value, null);
  }
}

5. trimToSize () 方法小结

  1. 该方法主要用于容量检测,和容量超出后的元素移除操作。
  2. 首先开启循序,并加锁,判断当前数据量 <= 设定的阀值 maxSize 则 break 跳出循环。
  3. 获取当前 LinkedHashMap 对象中的最老数据(最不常用的数据),但是源码中通过 for 循环去循环赋值,实际上循环完获取到的是最新的数据。

3.1. 举例说明,可通过以下例子和日志输出判断 for 循环后的最终赋值。

 LruCache<Integer, Integer> lruCache = new LruCache<>(5);
        lruCache.put(1, 1);
        lruCache.put(2, 2);
        lruCache.put(3, 3);
        lruCache.put(4, 4);
        lruCache.put(5, 5);

        for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
            Log.e("LRU", entry.getKey() + ":" + entry.getValue());
        }

        2019-11-19 16:22:54.299 31747-31747/com.we.we E/LRU: 1:1
        2019-11-19 16:22:54.299 31747-31747/com.we.we E/LRU: 2:2
        2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 3:3
        2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 4:4
        2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 5:5

3.2. 通过以上例子的日志打印,可以看到最后打印的也就是最后循环的数据为 5, 而 5 是最新插入的数据,那这个逻辑删除的不是最新插入的数据了么?那对 LRU 而言不是有问题了么?我又翻看了以前 SDK 版本的源码,接下来我们对比下 API 27 和 API 28 中该段逻辑的差异。

API 27
Map.Entry<K, V> toEvict = map.eldest();
//...
//eldest是被标记隐藏的. 实现如下:
public Entry<K, V> eldest() {
  LinkedEntry<K, V> eldest = header.nxt;
  return eldest != header ? eldest : null;
}

API 28
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
  toEvict = entry;
}

3.3. 通过对比因此这段逻辑是源码中的一个 bug,但是我们使用过程中并不影响正常的功能,这是为什么?其实是 Framework 实现和 SDK 源码不一致,而 SDK 中是一份带 bug 版本的 LruCache.

4. 移除数据并 size -1, 缓存移除的次数 +1

6. get () 方法源码解析

/**
 * 指定 key 对应的 value 值存在时返回,否则通过 create 方法创建相应的 key-value 对。
 * 如果对应的 value 值被返回,那么这个 key-value 对将被移到队尾。
 * 当返回 null 时,表明没有对应的 value 值并且也无法被创建
 */

    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            //通过LinkedHashMap对象获取当前key对应的value
            mapValue = map.get(key);
            if (mapValue != null) {
                //缓存命中的次数+1
                hitCount++;
                //并return返回结束
                return mapValue;
            }

            //缓存未命中的次数+1
            missCount++;
        }

        /*
         * 尝试创建一个value值,但是可能消耗很长时间,当create方法返回时,哈希表有几率发生变化
         * 如果再哈希表中添加了一个有冲突的值,那么将保留原有的值,替换刚刚生成的value
         */

         //create()是需要重写的方法,如果不重写默认返回null
        V creaweValue = create(key);
        if (creaweValue == null) {
            return null;
        }

        synchronized (this) {
            //创建 key 对应的 value 的次数+1
            createCount++;
            //map.put方法返回的是该key下原来的值,如果该key下原来没有值则返回null
            mapValue = map.put(key, creaweValue);

            if (mapValue != null) {
                //mapValue不为null说明该key原来有对应的值,因此需要使用原来值替代刚刚生成的value。
                map.put(key, mapValue);
            } else {
                //如果原来key没有对应的值,说明新增了一对键值对,因此需要size+1
                size += safeSizeOf(key, creaweValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, creaweValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return creaweValue;
        }
    }

7. get () 方法小结

  1. get () 放中除了正常的判 null 以为,进行了加锁并通过 LinkedHashMap.get (key) 获取该 key 下的 value。
  2. 如果 LinkedHashMap.get (key) 没有获取到 value,则通过 create () 方法创建对应的 value 值,但 create () 默认返回 null,需要重写进行创建处理。
  3. 如果重写了 create () 方法并且生成了 value,则加锁通过 LinkedHashMap.put () 进行插入,并根据 put 方法的返回值判断该 key 之前是否有对应的 value,如果有则重新将原有的值 put 替换刚刚生成的值。

以上逻辑中 create (key) 方法其实是未加锁的,如果该方法是有使用的,可能会出现多线程调用同一个 key,而生成多个 value 的情况。或者当一个线程调用 put 而另一个线程调用了 create 方法为其创建值时这种情况下,在后续逻辑中会调用 entryRemoved () 方法进行处理。而 entryRemoved () 方法也需要重写实现。可根据 entryRemoved () 方法的参数值判断当前调用是由 put 或 remove 引起的还是为了腾空间由 trimToSize () 引起的。

@param evicwe true 表明条目正在被删除以腾出空间,false 表明删除是由 put 或 remove 引起的(并非是为了腾出空间)

@param newValue key 的新值。如果非 null,则此删除是由 put 引起的。否则它是由 remove 引起的

8. remove () 源码解析

/**
 * 删除key对应的value
 *
 * 返回key对应的value值
 */
public final V remove(K key) {
  if (key == null) {
       throw new NullPointerException("key == null");
   }

  V previous;

  synchronized (this) {
  previous = map.remove(key);

  if (previous != null) {
    //数据删除后需要将数据计数器size -1 
    size -= safeSizeOf(key, previous);
  }
}

  if (previous != null) {
      entryRemoved(false, key, previous, null);
  }
     return previous;
}

9. remove () 方法小结

1. 判空,并加锁调用 LinkedHashMap.remove () 方法移除该 key 对应的 value,并通过返回值判断 size -1;

2. 调用 entryRemoved () 方法

总结

通篇下来发现 LruCache 有以下几个特点。

  • 其实 LruCache 类中方法数量不多,大部分都依赖于 LinkedHashMap 的方法,而 LruCache 只是在维护一个内部 LinkedHashMap 对象。
  • LruCache 使用并不是线程安全的,因此提供了 entryRemoved () 方法用来重写解决异常情况。
  • LruCache 的大部分方法中都进行了 K/V 判空,因此 LruCache 不支持空 K 和空 V。

感谢大家能耐着性子看完啰里啰嗦的文章

在这里我也分享一份私货,自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

如果你有需要的话,可以点赞+评论+转发关注我,然后私信我【进阶】我发给你

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码