[关闭]
@TryLoveCatch 2021-04-09T10:23:23.000000Z 字数 14995 阅读 2591

Android性能优化之xxx

android 性能优化


参考

Android APP 性能优化的一些思考

01 | 崩溃优化(上):关于“崩溃”那些事儿

知识

ANR

Android ANR问题总结

那么哪些场景会造成ANR呢?

理解Android ANR的触发原理

  • Service Timeout:比如前台服务在20s内未执行完成;
  • BroadcastQueue Timeout:比如前台广播在10s内未执行完成
  • ContentProvider Timeout:内容提供者,在publish过超时10s;
  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。

分析

  1. "main" prio=5 tid=1 Native
  2. | group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
  3. | sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
  4. | state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
  5. | stack=0xbe307000-0xbe309000 stackSize=8MB
  6. | held mutexes=
  7. kernel: (couldn't read /proc/self/task/3276/stack)
  8. native: #00 pc 0004099c /system/lib/libc.so (__epoll_pwait+20)
  9. native: #01 pc 00019f63 /system/lib/libc.so (epoll_pwait+26)
  10. native: #02 pc 00019f71 /system/lib/libc.so (epoll_wait+6)
  11. native: #03 pc 00012ce7 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
  12. native: #04 pc 00012f63 /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
  13. native: #05 pc 00086abd /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
  14. native: #06 pc 0000055d /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
  15. at android.os.MessageQueue.nativePollOnce(Native method)
  16. at android.os.MessageQueue.next(MessageQueue.java:323)
  17. at android.os.Looper.loop(Looper.java:138)
  18. at android.app.ActivityThread.main(ActivityThread.java:5528)
  19. at java.lang.reflect.Method.invoke!(Native method)
  20. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
  21. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)

当然这种情况很有可能是由于该进程的其他线程消耗掉了CPU资源,这就需要分析其他线程的trace以及ANR前后该进程自己输出的log了。

ChkBugReport

https://blog.csdn.net/gjsisi/article/details/12105145?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase

  1. adb bugreport /Users/xxx/

ANR reason主要有以下几类:

避免ANR

目标

从图中可以看到,打造一个高质量的应用应该以4个方向为目标:快、稳、省、小。

卡顿优化

Android性能优化 - 消除卡顿
Android 的屏幕刷新机制

什么是卡顿

iOS 渲染原理解析

垂直同步 Vsync + 双缓冲机制 Double Buffering

解决屏幕撕裂、提高显示效率的一个策略就是使用垂直同步信号 Vsync 与双缓冲机制 Double Buffering。根据苹果的官方文档描述,iOS 设备会始终使用 Vsync + Double Buffering 的策略。

垂直同步信号(vertical synchronisation,Vsync)相当于给帧缓冲器加锁:当电子束完成一帧的扫描,将要从头开始扫描时,就会发出一个垂直同步信号。只有当视频控制器接收到 Vsync 之后,才会将帧缓冲器中的位图更新为下一帧,这样就能保证每次显示的都是同一帧的画面,因而避免了屏幕撕裂。

但是这种情况下,视频控制器在接受到 Vsync 之后,就要将下一帧的位图传入,这意味着整个 CPU+GPU 的渲染流程都要在一瞬间完成,这是明显不现实的。所以双缓冲机制会增加一个新的备用缓冲器(back buffer)。渲染结果会预先保存在 back buffer 中,在接收到 Vsync 信号的时候,视频控制器会将 back buffer 中的内容置换到 frame buffer 中,此时就能保证置换操作几乎在一瞬间完成(实际上是交换了内存地址)。

掉帧 Jank

启用 Vsync 信号以及双缓冲机制之后,能够解决屏幕撕裂的问题,但是会引入新的问题:掉帧。如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况。

如图所示,A、B 代表两个帧缓冲器,当 B 没有渲染完毕时就接收到了 Vsync 信号,所以屏幕只能再显示相同帧 A,这就发生了第一次的掉帧。

三缓冲 Triple Buffering

事实上上述策略还有优化空间。我们注意到在发生掉帧的时候,CPU 和 GPU 有一段时间处于闲置状态:当 A 的内容正在被扫描显示在屏幕上,而 B 的内容已经被渲染好,此时 CPU 和 GPU 就处于闲置状态。那么如果我们增加一个帧缓冲器,就可以利用这段时间进行下一步的渲染,并将渲染结果暂存于新增的帧缓冲器中。

如图所示,由于增加了新的帧缓冲器,可以一定程度上地利用掉帧的空档期,合理利用 CPU 和 GPU 性能,从而减少掉帧的次数。

屏幕卡顿的本质
手机使用卡顿的直接原因,就是掉帧。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。

这样看来,可以大概总结一下

屏幕卡顿的根本原因:CPU 和 GPU 渲染流水线耗时过长,导致掉帧。
Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。
三缓冲的意义:合理使用 CPU、GPU 渲染性能,减少掉帧次数。

代码检测卡顿

Android UI性能优化 检测应用中的UI卡顿

利用loop()中打印的日志

大家都知道在Android UI线程中有个Looper,在其loop方法中会不断取出Message,调用其绑定的Handler在UI线程进行执行。

大致代码如下:

  1. public static void loop() {
  2. final Looper me = myLooper();
  3. final MessageQueue queue = me.mQueue;
  4. // ...
  5. for (;;) {
  6. Message msg = queue.next(); // might block
  7. // This must be in a local variable, in case a UI event sets the logger
  8. Printer logging = me.mLogging;
  9. if (logging != null) {
  10. logging.println(">>>>> Dispatching to " + msg.target + " " +
  11. msg.callback + ": " + msg.what);
  12. }
  13. // focus
  14. msg.target.dispatchMessage(msg);
  15. if (logging != null) {
  16. logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
  17. }
  18. // ...
  19. }
  20. msg.recycleUnchecked();
  21. }
  22. }

第15行代码,就是有可能发生卡顿的地方,注意这行代码的前后,有两个logging。如果设置了logging,会分别打印出“>>>>> Dispatching to”和“<<<<< Finished to”这样的Log。这样就给我们监视两次Log之间的时间差,来判断是否发生了卡顿。

  1. public final class Looper {
  2. private Printer mLogging;
  3. public void setMessageLogging(@Nullable Printer printer) {
  4. mLogging = printer;
  5. }
  6. }
  7. public interface Printer {
  8. void println(String x);
  9. }

所以,我们可以自己实现一个Printer,在通过setMessageLogging()方法传入即可。

  1. public class MyApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. Looper.getMainLooper().setMessageLogging(new Printer() {
  6. private static final String START = ">>>>> Dispatching";
  7. private static final String END = "<<<<< Finished";
  8. @Override
  9. public void println(String x) {
  10. if (x.startsWith(START)) {
  11. LogMonitor.getInstance().startMonitor();
  12. }
  13. if (x.startsWith(END)) {
  14. LogMonitor.getInstance().removeMonitor();
  15. }
  16. }
  17. });
  18. }
  19. }

LogMonitor的代码如下:

  1. public class LogMonitor {
  2. private static final long TIME_BLOCK = 1000L;
  3. private static LogMonitor sInstance = new LogMonitor();
  4. private HandlerThread mHandlerThread = new HandlerThread("log");
  5. private Handler mHandler;
  6. private LogMonitor() {
  7. mHandlerThread.start();
  8. mHandler = new Handler(mHandlerThread.getLooper());
  9. }
  10. private static Runnable mRunnable = new Runnable() {
  11. @Override
  12. public void run() {
  13. StringBuilder sb = new StringBuilder();
  14. StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
  15. for (StackTraceElement s : stackTrace) {
  16. sb.append(s.toString() + "\n");
  17. }
  18. Log.e("TAG", sb.toString());
  19. }
  20. };
  21. public static LogMonitor getInstance() {
  22. return sInstance;
  23. }
  24. public void startMonitor() {
  25. mHandler.postDelayed(mRunnable, TIME_BLOCK);
  26. }
  27. public boolean isMonitor() {
  28. return mHandler.hasCallbacks(mRunnable);
  29. }
  30. public void removeMonitor() {
  31. mHandler.removeCallbacks(mRunnable);
  32. }
  33. }

我们假设阀值是1000ms,也就是说超过1000ms的都算卡顿。
当“>>>>> Dispatching to”打印了,我们就发送一个延迟1秒钟的任务,这个任务会打印出造成卡顿的UI线程里的堆栈信息。
如果在1秒钟之内我们检测到了“<<<<< Finished to”打印,就会移除这个延迟1秒的任务。
如果1秒内没有检测到“<<<<< Finished to”打印,

BlockCanary就是基于这个原理实现的。

利用Choreographer

Android性能优化第(十 一)篇---卡顿分析,正确评测流畅度

Choreographer就是一个消息处理器,根据vsync 信号 来计算frame,而计算frame的方式就是处理三种回调,包括事件回调、动画回调、绘制回调。这三种事件在消息输入、加入动画、准备绘图layout 等动作时均会发给Choreographer。

有的时候会看到这样的log,系统帮助我们打印出了跳帧数:

  1. 02-07 19:47:04.333 17601-17604/zhangwan.wj.com.choreographertest D/dalvikvm: GC_CONCURRENT freed 143K, 3% free 9105K/9384K, paused 2ms+0ms, total 6ms
  2. 02-07 19:47:04.337 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 60 frames! The application may be doing too much work on its main thread.
  3. 02-07 19:47:11.685 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 85 frames! The application may be doing too much work on its main thread.
  4. 02-07 19:47:12.545 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 37 frames! The application may be doing too much work on its main thread.
  5. 02-07 19:47:14.893 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 37 frames! The application may be doing too much work on its main thread.
  6. 02-07 19:47:23.049 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 36 frames! The application may be doing too much work on its main thread.
  7. 02-07 19:47:23.929 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 37 frames! The application may be doing too much work on its main thread.
  8. 02-07 19:47:24.961 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 61 frames! The application may be doing too much work on its main thread.
  9. 02-07 19:47:25.817 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 36 frames! The application may be doing too much work on its main thread.
  10. 02-07 19:47:26.433 17601-17601/zhangwan.wj.com.choreographertest I/Choreographer: Skipped 36 frames! The application may be doing too much work on its main thread.

这个log就出自于Choreographer中

  1. public final class Choreographer {
  2. void doFrame(long frameTimeNanos, int frame) {
  3. final long startNanos;
  4. synchronized (mLock) {
  5. if (!mFrameScheduled) {
  6. return; // no work to do
  7. }
  8. //当前时间
  9. startNanos = System.nanoTime();
  10. //抖动间隔
  11. final long jitterNanos = startNanos - frameTimeNanos;
  12. //抖动间隔大于屏幕刷新时间间隔(16ms)
  13. if (jitterNanos >= mFrameIntervalNanos) {
  14. final long skippedFrames = jitterNanos / mFrameIntervalNanos;
  15. //跳过了几帧!,也许当前应用在主线程做了太多的事情。
  16. if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
  17. Log.i(TAG, "Skipped " + skippedFrames + " frames! "
  18. + "The application may be doing too much work on its main thread.");
  19. }
  20. //最后一次的屏幕刷是lastFrameOffset之前开始的
  21. final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
  22. if (DEBUG) {
  23. Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
  24. + "which is more than the frame interval of "
  25. + (mFrameIntervalNanos * 0.000001f) + " ms! "
  26. + "Skipping " + skippedFrames + " frames and setting frame "
  27. + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
  28. }
  29. //最后一帧的刷新开始时间
  30. frameTimeNanos = startNanos - lastFrameOffset;
  31. }
  32. //由于跳帧可能造成了当前展现的是之前的帧,这样需要等待下一个vsync信号
  33. if (frameTimeNanos < mLastFrameTimeNanos) {
  34. if (DEBUG) {
  35. Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
  36. + "previously skipped frame. Waiting for next vsync.");
  37. }
  38. scheduleVsyncLocked();
  39. return;
  40. }
  41. //当前画面刷新的状态置false
  42. mFrameScheduled = false;
  43. //更新最后一帧的刷新时间
  44. mLastFrameTimeNanos = frameTimeNanos;
  45. }
  46. //按照优先级策略进行画面刷新时间处理
  47. doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
  48. doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
  49. doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
  50. if (DEBUG) {
  51. final long endNanos = System.nanoTime();
  52. Log.d(TAG, "Frame " + frame + ": Finished, took "
  53. + (endNanos - startNanos) * 0.000001f + " ms, latency "
  54. + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
  55. }
  56. }
  57. }

当跳帧数大于设置的SKIPPED_FRAME_WARNING_LIMIT 值时会在当前进程输出这个log。由于 SKIPPED_FRAME_WARNING_LIMIT 的值默认为 30,所以上面的log并不是经常看到。

方法一

如果我们用反射的方法把SKIPPED_FRAME_WARNING_LIMIT的值设置成1,这样可以保证只要有丢帧,就会有上面的log输出来。

  1. static {
  2. try {
  3. Field field = Choreographer.class.getDeclaredField("SKIPPED_FRAME_WARNING_LIMIT");
  4. field.setAccessible(true);
  5. field.set(Choreographer.class, 1);
  6. } catch (Throwable e) {
  7. e.printStackTrace();
  8. }
  9. }

只要捕获这个log提取出skippedFrames 就可以知道界面是否卡顿。

方法二

上面的方法只能让我们知道丢帧了,却无法知道更详细定位问题的信息了。
在Choreographer中有个回调接口,FrameCallback。

  1. public interface FrameCallback {
  2. //当新的一帧被绘制的时候被调用。
  3. public void doFrame(long frameTimeNanos);
  4. }
  1. public class BlockDetectByChoreographer {
  2. public static void start() {
  3. Choreographer.getInstance()
  4. .postFrameCallback(new Choreographer.FrameCallback() {
  5. @Override
  6. public void doFrame(long l) {
  7. if (LogMonitor.getInstance().isMonitor()) {
  8. LogMonitor.getInstance().removeMonitor();
  9. }
  10. LogMonitor.getInstance().startMonitor();
  11. Choreographer.getInstance().postFrameCallback(this);
  12. }
  13. });
  14. }
  15. }

第一次的时候开始检测,如果大于阈值则输出相关堆栈信息,否则则移除。

方法三

Cockroach原理

卡顿的原因

Android 应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分为4类:

这4类卡顿场景的根本原因可以分为两种:

优化建议

布局优化

我们知道一个页面的测量布局都是通过递归来完成的,所以多叉树遍历的时间与树的高度h有关。
那布局优化有哪些方法呢,主要通过减少层级、减少测量和绘制时间、提高复用性三个方面入手。

避免过度绘制

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。

如何避免过度绘制呢,如下:

启动优化

通过对启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。启动主要完成三件事:UI 布局、绘制和数据准备。因此启动速度优化就是需要优化这三个过程:

合理的刷新机制

在应用开发过程中,因为数据的变化,需要刷新页面来展示新的数据,但频繁刷新会增加资源开销,并且可能导致卡顿发生,因此,需要一个合理的刷新机制来提高整体的 UI 流畅度。合理的刷新需要注意以下几点:

工具使用

Profile GPU Rendering

在手机开发者模式下,有一个卡顿检测工具叫做:Profile GPU Rendering(GPU呈现模式分析)

条形图说明

Android Profiler

shallow size & retained size

Shallow Size和Retained Size。

先借用一张图,这张图更能清楚的表示他们之间的计算关系:

B的shallow size = B;
B的retained size = B shallow size + C retained size + D retained size;

在举例之前,首先要了解JAVA对象在堆中的存储,我们以32位JVM虚拟机为例:
JAVA对象在堆中共有3个部分组成:

说到引用类型大小,
32位虚拟机下引用占据4byte.
64位虚拟机下如果不开启指针压缩,则引用占据8byte。

下面我们根据例子进行说明:

  1. public class TestObjSize {
  2. private int a = 1;
  3. private boolean b = true;
  4. private TestObjSize testObjSize;
  5. public static void main(String[] args) {
  6. Object object = new Object();
  7. TestObjSize test = new TestObjSize();//这个new出来的对象记为obj1
  8. test.testObjSize = new TestObjSize();//这个new出来的对象记为obj2
  9. System.out.println(object.hashCode());
  10. System.out.println(test.hashCode());
  11. try {
  12. Thread.sleep(3000000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }

我们先分析下TestObjSize的shallow size:

因为shallow size与实例变量是否有引用无关,所以:

shallow size = 4byte(对象头) + 4byte(类型指针) + 4byte(int a) + 1byte(boolean b) + 4byte(TestObjSize引用) = 17;

而17不是8的整数倍,所以会有7个byte的填充数据,最终TestObjSize类型的实例对象的 shallow size = 24;

根据例子,我们看到obj2对象的testObjSize = null,所以:

obj2的retained size = obj2的shallow size = 24;

而obj1对象的testObjSize为obj2,所以:

obj1的retained size = obj1的shallow size + obj2的retained size = 48;

最后附上heapdump!

其他

内存优化

减少对象的内存占用

内存抖动

延迟加载

避免内存泄漏。

布局优化

选择耗时少的布局 FrameLayout 》 LinearLayout 》 RelatveLayout
减少布局层级 merge
提高布局复用性 include
减少测量绘制时间 viewstub onDraw里面不要进行初始化操作
hierarchy ['haɪə.rɑrki]

绘制优化

onDraw里面不要初始化对象
避免onDraw复杂计算 执行大量的计算 职责分离 计算放到其他函数
避免过渡绘制 clip方法
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注