Android性能优化-响应优化

产生ANR的原因

在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的.造成ANR的首要原因是在主线程(UI线程)里面做了太多的阻塞耗时操
例如:文件读写、数据库读写、网络查询

解决ANR

不要在主线程(UI线程)里面做繁重的操作.

ANR分析

获取ANR产生的trace文件

ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:

$adb pull data/anr/traces.txt

分析traces文件

这里针对ANR分析常见的三种情况

普通阻塞导致的ANR

强行sleep thread产生的一个ANR.

----- pid 2976 at 2018-05-08 23:02:47 -----
Cmd line: com.luliangdev.dev  // 最新的ANR发生的进程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 产生ANR的那个函数调用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.luliangdev.dev.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.luliangdev.dev.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
  at com.luliangdev.dev.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

如上trace信息中的添加的中文注释已基本说明了trace文件该怎么分析。

CPU满负荷

这个时候你看到的trace信息可能会包含这样的信息:

Process:com.luliangdev.dev
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...

100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait

最后一句表明了CPU占用100%, 满负荷了。其中绝大数是被I/O操作占用了。一般来说会发现是方法中有频繁的文件读写或是数据库读写操作放在主线程来做。

内存不足

其实内存原因有可能会导致ANR, 例如如果由于内存泄露, App可使用内存所剩无几, 我们点击按钮启动一个大图片作为背景的activity, 就可能会产生ANR, 这时trace信息可能是这样的:

// 以下trace信息来自网络, 用来做个示例
Cmdline: android.process.acore

DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)

...

MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732

可以看到free的内存已所剩无几.这种情况可能更多的是会产生OOM的异常…

ANR处理方式

  • 主线程阻塞: 开辟单独的子线程来处理耗时阻塞事务.
  • CPU满负荷, I/O阻塞: I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
  • 内存不足: 增大VM内存, 使用largeHeap属性, 排查内存泄露等.

常用执行在主线程的操作

  • Activity的所有生命周期回调都是执行在主线程的.
  • Service默认是执行在主线程的
  • BroadcastReceiver的onReceive回调是执行在主线程的.
  • 没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的.
  • View的post(Runnable)是执行在主线程的.

子线程使用方式

  • RxJava(强烈推荐)使用文档
  • 继承Theard
    `
    class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {

      this.minPrime = minPrime;
    

    }

    public void run() {

      // compute primes larger than minPrime
       . . .
    

    }
    }

PrimeThread p = new PrimeThread(143);
p.start();

- 实现Runnable接口

class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}

public void run() {
    // compute primes larger than minPrime
     . . .
}

}

PrimeRun p = new PrimeRun(143);
new Thread(p).start();



- HandlerThread

Android中结合Handler和Thread的一种方式. 前面有云, 默认情况下Handler的handleMessage是执行在主线程的, 但是如果我给这个Handler传入了子线程的looper, handleMessage就会执行在这个子线程中的. HandlerThread正是这样的一个结合体:

// 启动一个名为new_thread的子线程
HandlerThread thread = new HandlerThread(“new_thread”);
thread.start();

// 取new_thread赋值给ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
  // 此时handleMessage是运行在new_thread这个子线程中了.
}

}




- IntentService

前面有关于IntentService的使用方式。
Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.


- **特别注意**

使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
`
因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.

ANR结语

对于ANR问题, 个人认为还是预防为主, 认清代码中的阻塞点, 善用线程.(学学RxJava还是很必要的)

卡顿


卡顿来源

用户对卡顿的感知, 主要来源于界面的刷新. 而界面的性能主要是依赖于设备的UI渲染性能. 如果我们的UI设计过于复杂, 或是实现不够好, 设备又不给力, 界面就会像卡住了一样, 给用户卡顿的感觉.

16ms原则

Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).
为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.

16ms-1

这就意味着, 我们需要在16ms内完成下一次要刷新的界面的相关运算, 以便界面刷新更新. 然而, 如果我们无法在16ms内完成此次运算会怎样呢?

例如, 假设我们更新屏幕的背景图片, 需要24ms来做这次运算. 当系统在第一个16ms时刷新界面, 然而我们的运算还没有结束, 无法绘出图片. 当系统隔16ms再发一次VSYNC信息重绘界面时, 用户才会看到更新后的图片. 也就是说用户是32ms后看到了这次刷新(注意, 并不是24ms). 这就是传说中的丢帧(dropped frame):

16ms-2

丢帧给用户的感觉就是卡顿, 而且如果运算过于复杂, 丢帧会更多, 导致界面常常处于停滞状态, 卡到爆.

卡顿原因

一般导致卡顿的原因有以下几种:

  • 布局复杂

    界面性能取决于UI渲染性能. 我们可以理解为UI渲染的整个过程是由CPU和GPU两个部分协同完成的.

    CPU负责UI布局元素的Measure, Layout, Draw等相关运算执行. GPU负责栅格化(rasterization), 将UI元素绘制到屏幕上

    如果我们的UI布局层次太深, 或是自定义控件的onDraw中有复杂运算, CPU的相关运算就可能大于16ms, 导致卡顿.

    ps.栅格化(rasterization): 所谓的栅格化,就是绘制那些Button,Shape,Path,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示,通俗点说,就是解决哪些复杂的XML布局文件和标记语言,使之转换成用户能看懂的图像,但是这不是直接转换,XML布局文件需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行栅格化,对于栅格化,跟OpenGL有关,栅格化是一个特别费时的操作。

  • 过度绘制(Overdraw)

    Overdraw: 用来描述一个像素在屏幕上多少次被重绘在一帧上.
    通俗的说: 理想情况下, 每屏每帧上, 每个像素点应该只被绘制一次, 如果有多次绘制, 就是Overdraw, 过度绘制了.

Android系统提供了可视化的方案来让我们很方便的查看overdraw的现象:
在”系统设置”–>”开发者选项”–>”调试GPU过度绘制”中开启调试:

Overdraw1

此时界面可能会有五种颜色标识:

Overdraw2

  • 原色: 没有Overdraw
  • 蓝色: 1次Overdraw
  • 绿色: 2次Overdraw
  • 粉色: 3次Overdraw
  • 红色: 4次及4次以上的Overdraw

    一般来说, 蓝色是可接受的, 是性能优的.

    所以要减少多次绘制,即可达到优化目的。

  • 频繁的GC

上面说的都是处理上的, CPU, GPU相关的. 实际上内存原因也可能会造成应用不流畅, 卡顿的.

执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿.

导致频繁GC有两个原因:

  • 内存抖动(Memory Churn), 即大量的对象被创建又在短时间内马上被释放.
  • 瞬间产生大量的对象会严重占用Young Generation的内存区域, 当达到阀值, 剩余空间不够的时候, 也会触发GC. 即使每次分配的对象需要占用很少的内存,但是他们叠加在一起会增加Heap的压力, 从而触发更多的GC.

这些GC操作可能会造成上面说到的丢帧, 如下:
gc

就会让用户感知到卡顿了.

一般来说瞬间大量产生对象一般是因为我们在代码的循环中new对象, 或是在onDraw中创建对象等. 所以说这些地方是我们尤其需要注意的…


 上一篇
Android性能优化-内存优化 Android性能优化-内存优化
常见内存泄漏解决方案 单例导致内存泄露 单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应
2017-05-02
下一篇 
Android性能优化-布局优化 Android性能优化-布局优化
布局层级 尽量减少布局层级和复杂度 尽量不要嵌套使用RelativeLayout. 尽量不要在嵌套的LinearLayout中都使用weight属性. Layout的选择, 以尽量减少View树的层级为主. 去除不必要的父布局. 善用T
2017-05-02
  目录