精通安卓性能优化-第一章(四)

来源:互联网 发布:网络奇兵1 编辑:程序博客网 时间:2024/05/17 06:10

缓存结果

当计算比较昂贵的时候,记录过去的结果使得将来可以更快的得到它是一个很好的方法。缓存的使用非常简单,通常的伪代码样式如Listing 1-10所示。

Listing 1-10 使用Cache

result = cache.get(n);    // 传入参数n作为keyif (result == null) {    // 结果没有在cache中,所以我们计算并且添加到cache    result = computeResult(n);    cache.put(n, result);    // n作为key,result作为value}return result;

计算Fibonacci序列的快速递归算法产生了许多重复的计算,可以从存储结果中获取极大的收益。比如,计算第50000个序列值需要计算第25000个序列值和第24999个序列值。计算第25000个序列值需要第12500个值和第12499个值,当计算第24999个值的时候再次需要计算同样的第12500个值和12499个值。Listing 1-11给出了一个更好的实现,使用cache。

如果你熟悉Java,你可能尝试使用HashMap作为cache,它会工作的很好。然而,Android定义了SparseArray,当key是一个整型值的时候比HashMap更加有效率:HashMap需要key的类型为java.lang.Integer,SparseArray使用基本类型int作为key。因此使用HashMap将会触发许多Integer对象的创建,SparseArray简单的避免了。

Listing 1-11 更快的递归实现,使用BigInteger, long和Cache

public class Fibonacci {    public static BigInteger computeRecursivelyWithCache (int n) {        SparseArray<BigInteger> cache = new SparseArray<BigInteger>();        return computeRecursivelyWithCache(n, cache);    }        private static BigInteger computeRecursivelyWithCache (int n, SparseArray<BigInteger> cache) {        if (n > 92) {            BigInteger fN = cache.get(n);                        if (fN == null) {                int m = (n/2) + (n&1);                BigInteger fM = computeRecursivelyWithCache(m, cache);                BigInteger fM_1 = computeRecursivelyWithCache(m - 1, cache);                if ((n&1) == 1) {                    fN = fM.pow(2).add(fM_1.pow(2));                } else {                    fN = fM_1.shiftLeft(1).add(fM).multiply(fM);                }                cache.put(n, fN);            }            return fN;        }        return BigInteger.valueOf(iterativeFaster(n));    }        private static long iterativeFaster (int n) {    // 看Listing 1-5的实现        }}

测量显示computeRecursivelyWithCache(50000)大约需要20毫秒,比computeRecursivelyFasterUsingBigIntegerAndPrimitive(50000)减少了大约50毫秒。显然,这个差值随着n增长而增长:当n等于200,000的时候,两个方法分别需要50毫秒和330毫秒左右。


因为减少了许多的BigInteger对象的分配,当使用cache的时候BigInteger是不可变对象的问题已经不再严重。然而,当计算fN的时候3个BigInteger对象仍然被创建(其中两个的生命周期非常短),所以使用可变的整型仍然能够提升性能。


尽管使用HashMap代替SparseArray会慢一些,但是好处是使代码不是安卓独有的,即可以在非安卓的环境里面使用完全一样的代码(不使用SparseArray)。

NOTE:安卓定义了许多sparse array的类型:SparseArray(整数映射到对象),SparseBooleanArray(映射整数到布尔值), SparserIntArray(映射整数到整数)。


android.util.LruCache<K,V>

另外一个值得提到的类是android.util.LruCache<K,V>,在Android 3.1(Honeycomb)引入,使分配缓存的时候定义最大size非常简单。任选的,你也可以重写sizeOf()方法去改变每个缓存项的大小是如何计算的。因为它从安卓3.1开始支持,如果你的目标安卓版本低于3.1,你最终将只能使用一个不同的类在你的应用中实现缓存。这是一个很需要考虑的场景,因为Android3.1在当前市场安卓设备上仅占很少的一部分。一个可以替代的方案是继承java.util.LinkedHashMap,重写removeEldestEntry方法。LRU cache首先丢弃最近很少使用的项目。在一些应用程序中,你可能有相反的需求,即缓存首先丢弃最近最常使用的项目。安卓现在没有定义像MruCache这样一个类,这并不奇怪,因为MRU缓存并不常用。

当然,cache用来保存信息而不是计算。cache的通常使用方式是去保存下载数据,比如图片,仍然需要严格控制内存的消耗。比如,基于一个标准去重写LruCache的sizeOf( )方法限制缓存的大小而不是简单的限制缓存项的数目。尽管我们主要讨论LRU和MRU策略,在你的cache可能需要不同的策略去最大化命中率。比如,你的缓存首先丢弃不需要消耗很多就可以重建的项,或者简单的随机丢弃项目。遵循务实的态度设计你的cache。一个简单的替换策略比如LRU可以产生巨大的结果,可以使你的资源集中于其他更加重要的问题。

我们已经看到了几个不同的技巧去优化Fibonacci序列的计算。每个技巧都有它的优点,没有一个是最优的。通常最好的结果通过组合多个不同的技巧去实现而不是依赖于其中的某一个。比如,一个更快的实现可能使用了预计算(一种缓存机制),甚至可能稍微修改公式。(Hint:比如当n是4的倍数的时候)。需要怎样去计算,使得F(Integer.MAX_VALUE)耗时小于100毫秒?

0 0
原创粉丝点击