Android性能优化

797 阅读9分钟

1.UI优化

Android 系统每隔 16ms 发出 VSYNC 信号触发对UI进行渲染,那么就要求每一帧都要在 16ms 内绘制完成(包括发送给 GPU 和 CPU 绘制到缓冲区的命令,这样就能够达到流畅的画面所需要的60fps。


我们在使用app的时候,会发现有多的时候,界面会出现卡顿不流畅的情况,是因为当前这个界面UI的处理超过了16ms,则会占用下一个16ms,这样就导致16ms * 2 都是显示的同一帧,也就是我看到的“卡了”


丢帧原因:

  • 布局层次不合理
  • 布局存在过度绘制
  • cpu或者gpu负责过重
  • 动画执行次数过多
  • 频繁GC,主要是内存抖动
  • UI线程执行耗时操作

等等

优化思路:

布局优化

1.选择耗费性能较少的布局

如果布局中即可使用LinearLayout也可以使用RelativeLayout,那就采用LinearLayout。因为RelativeLayout在绘制时需要对子View分别进行了竖直和水平方向的两次测量,而Linearlayout在绘制时是根据我们设置的方向分别调用不同的测量方法。注意一点如果LinearLayout中子View使用了layout_weight属性时同样需要对子View进行两次测量以确定最终大小(对此不了解的小伙伴们可以查看源码中onMeasureonLayout方法本文就不多贴源码)。LinearLayoutFrameLayout都是一种性能耗费低的布局。但是很多时候单纯通过一个LinearLayoutFrameLayout无法实现产品的效果,需要通过嵌套的方式来完成。这种情况下建议使用RelativeLayout,因为嵌套就相当于增加了布局的层级,同样会降低程序的性能。

2.提高布局的复用性

使用 <include>标签提取布局间的公共部分,通过提高布局的复用性从而减少测量和绘制时间。

3.减少布局的层级

<merge> 布局标签一般和 <include> 标签一起使用从而减少布局的层级。例如当前布局是一个竖直方向的LinearLayout,这个时候如果被包含的布局也采用了竖直方向的LinearLayout,那么显然被包含的布局文件中的LinearLayout是多余的,这时通过 <merge> 布局标签就可以去掉多余的那一层LinearLayout。

4.减少初次测量和绘制时间

  • ViewStub继承了View,它非常轻量级且宽和高都为0,因此它本身不参与任何的绘制过程,避免资源的浪费,减少渲染时间,在需要的时候才加载View。因此ViewStub的意义在于按需求加载所需的布局,在实际开发中,很多布局在正常情况下不会显示,比如加载数据暂无数据,网络异常等界面,这个时候就没必要在整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用时在加载,提高了程序初始化时的性能。
  • 布局属性 wrap_content 会增加布局测量时计算成本,应尽可能少用。

5.减少控件的使用

在绘制布局中,某些情况下我们可以省去部分控件的使用。如TextView文字加图片。

层叠太多,过度绘制

1.调试 GPU 过度绘制

蓝色,淡绿,淡红,深红代表了4种不同程度的 Overdraw 情况,我们的目标就是尽量减少红色 Overdraw,看到更多的蓝色甚至白色区域。


优化:

  • 去除重复或者不必要的 background
  • 点击态中的 normal 尽量设置成 transparent
  • 去除 window 中的 background(这个可以通过处理 decorView 或者设置 Theme 的方式)
  • 若是自定义控件的话,通过 canvas.clipRect() 帮助系统识别那些可见的区域。

2.负载过重

UI 线程是应用的主线程,很多的性能和卡顿问题是由于在主线程中做了大量的工作。除了主线程外,子线程占用过多 CPU 资源也会导致渲染性能问题。

在 UI 渲染的过程中,是 CPU 和 GPU 共同合作完成的,其中 CPU 负责把 UI 组件计算成 Polygons,Texture 纹理,然后交给 GPU 进行栅格化渲染。


通过在 Android 设备的开发者选项里启动 “ GPU 呈现模式分析 ” ,可以得到最近 128 帧 每一帧渲染的时间。在 Android 6.0 之前,界面上显示的柱状图主要是三个颜色,分别是黄、红和蓝色。


通俗点来讲,黄色代表 CPU 通知 GPU,当 CPU 有太多事情做的时候,黄色的线就会长一些;红色代表渲染时间,比如层次深的情况下,渲染时间就会长一点,红色的线也会长一些;蓝色代表执行 onDraw() 时间。而横着的绿色的那条线代表 16ms 分割线。

通过Android System Trace性能数据采样和分析工具,可帮助开发者收集Android关键子系统(如Surfaceflinger、WindowManagerService等framework部分关键模块、服务)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。


从图片上来看,在加载页面的时候发生过好几次丢帧的情况,可以通过方法开查看具体什么原因导致的丢帧。Frames 是提供的判断绘制该帧的情况,分别有绿、黄和红色,当为空色的时候表示该帧耗时很严重,我们就可以从这些红色的 F 为出发点去分析。

内存抖动


内存抖动主要导致原因是频繁创建大对象或者频繁创建大量对象,并且这些对象属于用完就废弃的,比如 byte[] 。而 GC 操作或多或少都会 “ stop-the-world “,比如 GC 操作花费了 5ms 的时间,那么该帧的绘制就会从原来的 16ms 变为 11ms

优化:

  • 大对象可以使用对象池复用,比如 byte[]
  • 尽量在 16ms 内少创建对象,比如在 onDraw 中创建 Paint 对象,decode Bitmap 之类的
硬件加速

并非所有的都支持硬件加速,其中包括 clipPath() 等;同时也有一些方法在开启硬件加速之后与不开启硬件加速效果不一样,比如 drawBitmapMesh() 等。

Application 级别

<applicationandroid:hardwareAccelerated = "true" ...>

Activity 级别

<activity android:hardwareAccelerated = "true" ...>

Window 级别

getWindow().setFlags(
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

View 级别

View.setLayerType(View.LAYER_TYPE_HARDWARE, null);

2.内存优化

当一个对象在程序执行过后已经不需要再使用了,但是有其他的对象还持有该对象的引用,以致该对象不能被GC回收,那么这个对象会一直占用内存,从而导致该内存不可用,这种本该被GC回收(不再需要用了)而又不能被回收(被其他对象持有引用),以致停留在堆内存中的对象就造成了内存泄露。内存溢出(OutOfMeory),即我们通常所说的OOM,是指程序在申请内存时,没有足够的内存空间共其使用。

过多的内存泄露最终会导致内存溢出(OOM),内存泄露导致内存不足,会触发频繁GC,从而导致UI卡顿。

优化思路:

单例

主要原因是因为一般情况下单列都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放.

// 使用了单例模式 
// 如果Context使用的是Activity的Context,则会造成内存溢出
// 单例的Context 使用Application的Context,单例的生命周期和应用的一样长,这样基本可以防止单例引起来的内存泄露内存泄漏。
public class AppManager { 
    private static AppManager instance;  
    private Context context; private AppManager(Context context) { 
        this.context = context; 
     }  
    public static AppManager getInstance(Context context) {  
     if (instance != null) {  
         instance = new AppManager(context); 
      }  
      return instance;  
    }  
}
非静态内部类创建静态实例造成的内存泄漏
非静态内部类默认会持有外部类的引用,而改非静态内部类有创建了一个静态实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Actvity的引用,从而导致Activity的内存资源不能被正常的回收。

将改内部类设为静态内部类或者将该内部类抽取出来封装成一个单列,如果需要使用Context,就使用Application的Context。

public class MainActivity extends AppCompatActivity { 
    private static TestResource mResource = null;  

    @Override protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main);  
        if(mResource == null){  
            mResource = new TestResource(); 
         }  
         //...  
         }  
class TestResource { //... } }

Handler造成的内存泄露
当MainActivity结束时,未处理的消息持有Handler的引用,而handler又持有他所属的外部类也就是MainActivity,这条引用关系会一直保持知道消息得到处理,这样阻止了MainActivity内垃圾回收器回收,从而造成内存泄漏。

将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄露。

public class MainActivity extends AppCompatActivity {  
        private final Handler handler = new Handler() {   
        @Override public void handleMessage(Message msg) {  
        //  
        ... } 
        };  
         @Override protected void onCreate(Bundle savedInstanceState) {  
               super.onCreate(savedInstanceState);  
               setContentView(R.layout.activity_main); 
                new Thread(new Runnable() {  
                @Override public void run() { 
                 // ...  
                 handler.sendEmptyMessage(0x123);  
              } 
        });  
}

资源未关闭
  • 比如在Activity中register了一个BroadcastReceiver,但是Activity结束后没有unregister该BroadcastReceiver
  • 资源性对象比如Cursor,Stream,File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时关闭内存,它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外,如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露.
  • 对于资源性对象在不使用的时候,应该调用他的close()函数将其关闭,然后再设置为null,在我们的程序退出时,一定要确保我们的资源性对象已经关闭
  • Bitmap对象不再使用的时候调用recycle(),2.3以后的bitmap应该不再需要手动recycle()了,内存已经在java层了
使用轻量的数据结构
使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
用StringDef和IntDef来代替枚举(Enum)
枚举占用的内存过大,google官方建议用注解StringDef和IntDef来替换枚举
Bitmap处理
  • 对Bitmap压缩
  • Lru机制处理Bitmap
  • 使用有名的图片缓存框架(我一般使用这种)
不要使用String进行字符串拼接
  • 严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄漏
  • 频繁的字符串拼接,使用StringBuffer或者StringBuiler代替String,可以在一定程度上避免OOM和内存抖动
谨慎使用static对象
static对象的生命周期过程,应该谨慎使用