美文网首页Android面试首页推荐
Android优化(一)_Java代码优化_从优化斐波那契数列带

Android优化(一)_Java代码优化_从优化斐波那契数列带

作者: 影响身边的人 | 来源:发表于2016-08-01 23:22 被阅读791次

    性能优化总纲:

    大概会花一个月左右的时间出7-8个专题来分享一下在工作和学习中积累下来的android性能优化经验

    希望大家会持续关注。

    现在是专题一:java代码优化

    但这也仅仅是为大家提供一些思路与较为全面的总结,算不上什么,希望有错误或问题在下面评论。

    最后完结以后会将思维导图与优化框架整理出来,请期待。

    题记:

    如何确保Java应用在Android设备上获得高性能?首先要做的:知道Android是如何来执行代码的,然后再体会一下所谓的优化技巧,以及一些提高应用响应速度和高效使用数据库的技巧。

    不过,你应该意识到,代码优化不是应用开发者的首要任务,提供良好的用户体验并且专注于代码的可维护性,才是我们的首要任务。事实上,代码优化应该最后才做,如果你的程序自我感觉达到一个可以接受的水平,甚至不需要代码优化。

    一、我们先来看看Android是如何来执行代码的

    • Android→Java代码→Java字节码→Dalvik字节码→Dalvik虚拟机(4.4之前)

    • Android→Java代码→Java字节码→机器码(5.0之后)

    i: Android 4.4 中谷歌为开发者提供了两种编译模式,一种是默认的Dalvik模式,而另外一种则是ART模式,5.0废弃Dalvik。

    ii:本地代码直接由CPU执行,而不必由虚拟机解释执行;本地代码可以为特定架构予以优化。

    iii:从用户的角度来看,如果可以在100ms或者更短的时间内计算完成,那就是瞬时计算。

    JIT与Dalvik

    JIT是"Just In Time Compiler"的缩写,就是"即时编译技术",与Dalvik虚拟机相关。


    怎么理解这句话呢?这要从Android的一些特性说起。

    JIT是在2.2版本提出的,目的是为了提高Android的运行速度,一直存活到4.4版本,因为在4.4之后的ROM中,就不存在Dalvik虚拟机了。

    我们使用Java开发android,在编译打包APK文件时,会经过以下流程

    • Java编译器将应用中所有Java文件编译为class文件
    • dx工具将应用编译输出的类文件转换为Dalvik字节码,即dex文件
    • 之后经过签名、对齐等操作变为APK文件。

    Dalvik虚拟机可以看做是一个Java VM,他负责解释dex文件为机器码,如果我们不做处理的话,每次执行代码,都需要Dalvik将dex代码翻译为微处理器指令,然后交给系统处理,这样效率不高。

    为了解决这个问题,Google在2.2版本添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。

    当然使用JIT也不一定加快执行速度,如果大部分代码的执行次数很少,那么编译花费的时间不一定少于执行dex的时间。Google当然也知道这一点,所以JIT不对所有dex代码进行编译,而是只编译执行次数较多的dex为本地机器码。

    有一点需要注意,那就是dex字节码翻译成本地机器码是发生在应用程序的运行过程中的,并且应用程序每一次重新运行的时候,都要做重做这个翻译工作,所以这个工作并不是一劳永逸,每次重新打开App,都需要JIT编译。

    另外,Dalvik虚拟机从Android一出生一直活到4.4版本,而JIT在Android刚发布的时候并不存在,在2.2之后才被添加到Dalvik中。


    ART与AOT

    AOT是"Ahead Of Time"的缩写,指的就是ART(Anroid RunTime)这种运行方式。


    前面介绍过,JIT是运行时编译,这样可以对执行次数频繁的dex代码进行编译和优化,减少以后使用时的翻译时间,虽然可以加快Dalvik运行速度,但是还是有弊病,那就是将dex翻译为本地机器码也要占用时间,所以Google在4.4之后推出了ART,用来替换Dalvik。

    在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

    ART的策略与Dalvik不同,在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。之后打开App的时候,不需要额外的翻译工作,直接使用本地机器码运行,因此运行速度提高。

    当然ART与Dalvik相比,还是有缺点的。

    • ART需要应用程序在安装时,就把程序代码转换成机器语言,所以这会消耗掉更多的存储空间,但消耗掉空间的增幅通常不会超过应用代码包大小的20%
    • 由于有了一个转码的过程,所以应用安装时间难免会延长

    但是这些与更流畅的Android体验相比而言,不值一提。

    通过前面背景知识的介绍,我终于可以更简单的介绍这四个名词之间的关系了:

    • JIT代表运行时编译策略,也可以理解成一种运行时编译器,是为了加快Dalvik虚拟机解释dex速度提出的一种技术方案,来缓存频繁使用的本地机器码
      .
    • ART和Dalvik都算是一种Android运行时环境,或者叫做虚拟机,用来解释dex类型文件。但是ART是安装时解释,Dalvik是运行时解释
      .
    • AOT可以理解为一种编译策略,即运行前编译,ART虚拟机的主要特征就是AOT

    二、几点优化技巧

    优化思路一:

    微小的优化:当n等于0或者1的时候直接返回n,而不是在另外一个if语句中来检查n是否等于0或1.

    public class Fibonacci{
         public static long computeRecursively(int n){
              if(n>1) return computeRecursivelv(n-2) + computeRecursivelv(n-1);
              return n;
         }
    }
    

    优化思路二:以优化斐波那契数列为例,简单谈谈思想


    1、首次优化是消除一个方法调用

    public class Fibonacci{
         public static long computeRecursively(int n){
              if(n>1) {
                   long result = 1;
                       do {
                            result += computeRecursivelyWithLoop(n-2);
                            n--;
                           }while (n>1)
                            return result;
                           }
                           return n;
                          }
                }
    

    2、第二次优化会换成迭代实现:尤其是在没有多少内存的时候,递归算法往往要消耗大量栈空间,有可能导致栈溢出,让应用崩溃。

    public class Fibonacci{
         public static long computeRecursively(int n){
              if(n>1) {
                   long a= 0,b = 1;
                   do {
                    long tmp = b;
                    b += a;
                    a = amp;
        
                   }while (--n>1)
                    return b;
               }
       return n;
      }
    }
    

    3、 到三次稍加修改,每次迭代计算两项,迭代总数少了一半。由于long型只有64位,在斐波拉契数列的第92项,会出现溢出,导致结果错误,第93项会变成负的。

        public class Fibonacci{
             public static long computeRecursively(int n){
              if(n>1) {
                   long a= 0,b = 1;
               n--;
               a = n & 1;
               n /= 2;
    
    
               while (n-->0){
                a += b;
                b += a;
       
               }
               return b;
              }
              return n;
    }
    

    4、第四次用BigInteger,保证了不会溢出,但是速度再一次降了下来:1、BigInteger是不可变的 2、BigInteger使用BigInt和本地代码实现 3、数字越大,相加运算所花的时间越大

    public class Fibonacci{
     public static BigInteger computeIterativelvFasterUsingBigInteger(int n){
      if(n>1) {
       BigInteger a,b = BigInteger.ONE;
       n--;
       a = BigInteger.valueOf(n & 1);
       n /= 2;
    
    
       while (n-->0){
        a=a.add(0);
        b=b.add(a);
       
       }
       return b;
      }
      return n==0?BigInteger.ZERO : BigInteger.ONE;
    }
    

    5、第五次改进算法来减少分配数量。基于斐波那契Q-矩阵,我们会有一个算法公式来加快速度。


    6、第六次使用BigInteger和基本类型Long的快速递归实现

                当n>92时才使用BigInteger来进行运算,这样我们做以上运算会快20倍。
    

    7、第七次使用BigInteger和预先计算结果递归快速实现......

    > 好了,到了这里应该发现优化往往使源代码更难于阅读、理解和维护,而且,会有越来越少的人来能理解你写的代码的含义,代码复杂到笔者已经不想写了。而且,好的算法是无穷无尽的,只不过可能更复杂罢了,那我们费尽力气计算出来的结果,不能白白浪费(代价太高了),所以我们考虑到了缓存。
    
    result = cache.get(n);//输入参数n作为键
    
    if(result = null){
    
        //如果在缓存中没有result值,就计算出来存进去    
    
        result = computeResult(n);
    
        cache.put(n,result);//n作为键,result作为值
    
    }
    
    return result;
    
    
    

    8、考虑计算代价过高,最好把结果缓存起来,安卓定义的SparseArray类,比HashMap更高效(Integer和int区别)

    public class Fibonacci{
     public static BigInteger computeRecursivelyWith Cache(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);
         BigIntger fM = computeRecursiveWithCache(m,cache);
         BigIntger fM_1 = computeRecursiveWithCache(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){
       ...
      }
        } 
    }
    

    另外值得一提的是LRUCache算法,同样对应着一个MRUCache算法

    这个类是Android3.1引入的,可以在创建的时候自定义缓存的长度,另外,可以通过复写sizeof()方法改变每个缓存条目计算大小的方式。

    • LRU(Least Recently Used)缓存县丢弃最近最少使用的项目,不过在某些分情境中我们还可能用到MRUcache丢弃最近最多使用的项目。这两种算法现在在这里不深入讨论,等以后有机会分享数据结构在详谈。

    最后我们得出一个结论,我们对一个场景进行优化,往往有很多方式,但是某一种实现一般不是最好的解决方式,最好的结果就是结合多种不同的技术,而不是只依赖于其中一个,例如更快的实现可以用预计算、缓存机制、甚至采用不同的数学公式。


    三、API

    一般我们在manifest中应该使用<uses-asd>元素制定以下三个重要的信息

    • 最低API等级(mainSdkVersion)
    • 期望API等级(targetSdkVersion)
    • 最高API等级(maxSdkVersion)

    要注意一点 :过期的不能使用

    另外注意:可以用新API来获取最好的性能,也可以在旧平台上正常运行

    例如:

    android6.0权限问题

    android3.0以下兼容属性动画等。

    sparseArray的使用:

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
    
        sparseArray.removeAt(1);//11以上
    
    } else {
    
        int key = sparseArray.keyAt(1);//默认实现慢一些
    
        ​sparseArray.remove(key);
    
    }
    

    如不想用上面的方法来检测版本号,还可以用反射来确认是否有特定方法,但是有一点,在性能至关主要的地方应尽量避免使用反射。替代的办法是在静态初始化代码块里调用Class.forName()和Class.getMethod()确认指定方法时否存在,在性能要求高的地方只调用Method.invoke()就好了。


    四、数据结构

    通过上面斐波那契数列实现证明,好的算法和数据结构是实现快速应用的关键。java.util包中已经定义好了很多我们可以随手拿来用的工具了,比如各种集合。Android还定义了一些为了解决性能问题而生的类:

    • LruCache
    • SparseArray
    • SparseBooleanArray
    • SparseIntArray
    • Pair
    数据结构还是和上面一样等以后有机会再讨论。

    五、响应能力

    让用户真正感觉到快才行,比如延迟加载技术

    通常我们的做法是在组件的onCreate()方法中执行所有初始化。虽然这样做可行,但这意味着onCreate()需要较长的时间才能结束。

    这一点对应用的ACtivity尤为重要,onStart()直到onCreate()方法之后才会被调用(同样,onResume()只有在onStart()完成之后才会被调用).

    任何的延迟都会导致应用需要较长时间才嗯那个启动,用户最终可能会感到难以忍受。

    • 让你的主线程只做下面这种事情:

      • 按键接收

      • 绘制View

      • 产生生命周期方法

    来简单说一下用户感受:当用户感觉到你的应用有卡顿的时候,好感度就会降低,到一定临界点后,就再见了.

    那么,什么时候才是感觉到卡顿的,一般我们人眼看到的图像帧率为60fps的时候,会感到比较流畅,换算成时间就是0.016s/帧,如果你的应用某个点再0.016s之内没有渲染完成,就会造成所谓的卡顿,那么从优化的角度来说,除了改变GPU,我们能做的事情,就是减少布局的嵌套与ViewStub推迟对象创建。当然你可以用视图树来检测,那不是在代码优化的范围内了,所以知道就好。

    Android使用android.view.ViewStub来推迟初始化,它可以在运行时展开资源。当View-Stub需要展现时,它被相应的资源展开替换,自己就成为得待垃圾回收的对象。

    由于内存分配需要花时间,等到对象真正需要时才进行分配,也是一个很好的选择。当某个对象并不是立即就要使用时,推迟创建对象有着很明显的好处。下面代码是退出初始化的示例:为了避免总是检查对象是否为空,考虑使用工厂方法模式。

    int n = 100;
    if(cache == null){
     //createCache分配缓存对象,可以从许多地方调用它
     cache = createCache();
    }
    BigInteger fN = cache.get(n);
    if(fN == null){
     fN = Fibonacci.computeRecurivelyWithCache(n);
     cache.put(n,fN);
    }
    

    六、SQLite:

    大多数应用都不会是SQLite的重度使用者,因此,不用太担心与数据库打交道时的性能(对数据库有大量使用请参考我的另一篇文章《三个方面解决性能问题》)。不过。在优化应用中SQLite相关的代码时,需要了解几个概念:

    1、SQLite语句
    
    2、事务
    
    3、查询
    

    因为普通的sql语句是简单的字符串,需要解释或者编译才可以执行。当你执行SQL语句时,例如:

    SQLiteDatabase db = SQLiteDatabase.create(null);//数据库在内存中
    db.execSQL("CREATE TABLE cheese(name TEXT,origin TEXT)");
    db.execSQL("INSERT INTO cheeese VALUES ('Roquefort','Roquefort-sur-Solulzon')");
    db.close();//关闭数据库
    

    事实证明,执行SQLite的语句可能需要一段较长时间。除了编译,语句本身还需要创建。现在我们只关心INSERT的性能,毕竟,表只会创建一次,但会添加,修改,删除多次。

    例如:String sql = "INSERT INTO cheese VALUES(\"" +name +"\",\"" + origin +"\")"';
    
    • 这样的650条数据到内存数据库中用时393ms,平均一条0.6ms。

    那么第一个优化方案是用StringBuilder或String.format()来替代“+”

    builder.appended(name).append("\",\"").addped(origin).append("\")");
    String sql = String.format("INSERT INTO cheese VALUES(\"%s\",\"%s\")")",name .origin);
    
    
    
    
    
    • 上面这两个方案大概能快几十毫秒。

    第二种方案:我们发现所有的语句都非常相似,所以可以使用一个语句,让一部分在循环外只编译一次:

    public void populateWithCompileStatement(){
     SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
     int i = 0;
         for(String name : sCheeseNames){
              String origin = sCheeseOrigins[i++];
              stmt.clearBingings();
              stmt.bindString(1,name);//替换第一个问号name
              stmt.bingString(2,origin);//替换第二个问号为origin
              stmt.executeInsert();
         }
    }
    
    
    • 因为只进行了一次语句编译,而不是650次,并且绑定值是比编译更轻量级的操作,所以这种方法明显快多了、总共用时269ms。

    事务:上述例子并没有显示创建任何事务,但会自动为每个插入操作创建一个事务,并在每次插入后立即提交。显示创建事务有以下两个基本特征:

    原子提交
    
    性能更好
    

    抛开对性能的追求,第一个特性是很重要的。原子提交意味着数据库的所有修改都完成或都不做。事务不会只提交部分修改。如上面代码,加入事务之后

    try{
    
    db.beginTransaction();
    
    SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)");
     int i = 0;
         for(String name : sCheeseNames){
              String origin = sCheeseOrigins[i++];
              stmt.clearBingings();
              stmt.bindString(1,name);//替换第一个问号name
              stmt.bingString(2,origin);//替换第二个问号为origin
              stmt.executeInsert();
         }
    
    db.setTransactionSuccessful();//删除这一调用不会提交任何改动!
    
    } catch(e..){
    
    //异常处理
    
    }finally{
    
    db.endTransaction();//必须写在finally里
    
    }
    
    
    

    查询:我们可以用限制数据库的访问方式来加快查询速度,尤其是对存储中的数据库。数据库查询仅会返回一个cursor(游标)对象,然后用它来遍历结果。

    查询的时候,尽量只读取需要的数据。例如假设我们的表有两列,name和origin:

    db.query("cheese,null,null,null,null,null,null");(1)
    
    db.query("cheese",new String[]{"name"},null,null,null,null,null);(2)
    

    查询一定量的数据两种方法分别用时61ms,23ms

    所以,可以肯定,只读取需要的数据才是上上之选。


    七、总结:

    几年前,java由于性能问题而广受诟病,现在情况已大有改观。

    每次发布新版本Android时,Dalvik虚拟机(包括它的JIT编译器)的性能都会有所提升。

    代码可以编译为本地代码,从而利用最新的CPU架构,而不必重新编译。

    虽然实现很重要,但最重要的还是慎选数据结构和算法。

    好的算法可以弥补差的实现,甚至不需要优化就可以使应用流畅运行;而坏的算法无论你在实现上花费多少精力,其结果还是会很糟糕。

    最后,响应顺畅是成功的关键。

    相关文章

      网友评论

      本文标题:Android优化(一)_Java代码优化_从优化斐波那契数列带

      本文链接:https://www.haomeiwen.com/subject/gdlfsttx.html