性能优化篇

背景

最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个系统的梳理,整理成一个系列:Android应用开发岗 面试汇总。本系列将分为以下几个大模块:
Java基础篇、Java进阶篇、常见设计模式
Android基础篇、Android进阶篇、性能优化
网络相关、数据结构与算法
常用开源库、Kotlin、Jetpack

注1:以上文章将陆续更新,直到我找到满意的工作为止,有跳转链接的表示已发表的文章。
注2:该系列属于个人的总结和网上东拼西凑的结果,每个知识点的内容并不一定完整,有不正确的地方欢迎批评指正。
注3:部分摘抄较多的段落或有注明出处。如有侵权,请联系本人进行删除。

性能优化篇只做概述,详细优化方案参考链接中的文章即可

1 启动速度优化

前置知识点:APP启动流程、Activity启动流程

优化方向
Google给出了启动加速的方向:

  • 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
  • 避免在启动时做密集沉重的初始化(Heavy app initialization);
  • 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。

1.1 替换Application的主题

1、当APP启动时,会先于Main Thread展示一个空白的window,此时,可以通过给Application设置特定的主题,即展示一个图片,用到的是theme中的windowBackground属性。



2、再在Activity的onCreate方法中,先于super.onCreate()把正常的主题替换回来。


image.png

1.2 代码逻辑优化

  • 异步初始化组件
  • 梳理业务逻辑,延迟初始化组件、操作
  • 正确使用线程(1、设置子线程的优先级;2、避免在启动时创建线程池)
  • 去掉无用代码、重复逻辑等。

1.3 使用耗时统计工具

  • BlockCanary:收集卡顿的堆栈信息
  • hugo:通过注解的方式,统计单个方法的耗时
  • Android Profiler:AS提供的性能优化工具,这里使用CPU这块的Start Method Tracing来生成.trace文件,再通过DDMS来查看,可以看到具体线程中的时间轴上,各个方法的耗时情况。
    参考链接

1.4 冷启动耗时统计

1.4.1 adb命令 :

adb shell am start -S -W 包名/启动类的全限定名 。-S 表示重启当前应用

C:\Android\Demo>adb shell am start -S -W com.example.moneyqian.demo/com.example.moneyqian.demo.MainActivity
Stopping: com.example.moneyqian.demo
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.moneyqian.demo/.MainActivity }
Status: ok
Activity: com.example.moneyqian.demo/.MainActivity
ThisTime: 2247
TotalTime: 2247
WaitTime: 2278
Complete
  • ThisTime : 最后一个 Activity 的启动耗时(例如从 LaunchActivity - >MainActivity「adb命令输入的Activity」 , 只统计 MainActivity 的启动耗时)
  • TotalTime : 启动一连串的 Activity 总耗时.(有几个Activity 就统计几个)
  • WaitTime : 应用进程的创建过程 + TotalTime .

1.4.2 系统日志 :

在AS中的Logcat中,过滤关键字:ActivityTaskManager: Displayed(华为系统关键字不同),可以看到当前启动的activity以及耗时,如:

image.png

参考链接

2 布局优化

根据Google官方出品的Android性能优化典范,60帧每秒是目前最合适的图像显示速度,事实上绝大多数的Android设备也是按照每秒60帧来刷新的。为了让屏幕的刷新帧率达到60fps,我们需要确保在时间16ms(1000/60Hz)内完成单次刷新的操作(包括measure、layout以及draw),这也是Android系统每隔16ms就会发出一次VSYNC信号触发对UI进行渲染的原因。

2.1 帧率,丢帧

  • 帧率:60fps:即每秒钟屏幕刷新60次,也就是每16ms刷新一次。如果刷新操作在16ms内顺利完成则可以展示出流畅的画面;
  • 丢帧:然而由于任何原因导致接收到VSYNC信号的时候无法完成本次刷新操作,就会产生掉帧的现象,刷新帧率自然也就跟着下降(假定刷新帧率由正常的60fps降到30fps,用户就会明显感知到卡顿)。

2.2 过渡绘制

  • 理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,Overdraw由此产生。
  • 我们可以通过调试工具来检测Overdraw:设置——开发者选项——调试GPU过度绘制——显示过度绘制区域。该工具会把不同绘制次数的区域通过不同颜色展示出来。

原色 – 没有过度绘制 – 这部分的像素点只在屏幕上绘制了一次。
蓝色 – 1次过度绘制– 这部分的像素点只在屏幕上绘制了两次。
绿色 – 2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。
粉色 – 3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。
红色 – 4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。

2.3 过渡绘制优化方案

过渡绘制需要结合业务,适当的进行优化

  • 去掉不需要设置背景色的控件的背景色
  • 去掉Activity主题的背景色
  • 减少嵌套层次及控件个数
  • 通过Hierarchy Viewer工具查看布局层级,及Measure、Layout、Draw的耗时情况
  • 使用约束布局替换原有布局,以减少层级

2.4 使用GPU配置渲染工具

  • 打开设备的GPU配置渲染工具——》在屏幕上显示为条形图,可以协助我们定位UI渲染问题。
  • GPU配置渲染工具虽然可以定位出问题发生在某个步骤,但是并不能定位到具体的某一行;当我们定位到某
    个步骤之后可以使用工具TraceView进行更加详细的定位。

2.5 其他优化方案

使用标签,Merge减少嵌套层次、ViewStub延迟初始化。

参考链接

3 内存优化

内存优化,即对出现内存的问题进行解决。Android中常见的内存问题包括:

  • 内存泄露
  • 内存溢出
  • 内存抖动

3.1 内存泄露

内存泄露的本质,即短生命周期的对象被长生命周期对象引用,导致不能被回收。
解决方案:

  • 集成LeakCanary,可以方便的定位出90%的内存泄漏问题;
  • 单例中使用applicationContext,可解决大部分因为context导致的内存泄露问题
  • 及时关闭IO、关闭数据库cursor、释放Bitmap(bitmap.recycle())、Handler中使用弱引用获取activity实例

3.2内存溢出

Android系统的每个进程都有一个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出OOM错误。
解决方案:

  • 避免内存泄露,内存泄漏的存在可能导致可用内存越来越少
  • 对内存的状态进行监听,在Activity中覆写此方法,根据不同的case进行不同的处理
@Override
    public void onTrimMemory(int level) { //level有多个值,具体略
        super.onTrimMemory(level);
    }

3.3 内存抖动

  • 大量的对象被创建又在短时间内马上被释放,导致内存使用量上下波动。
  • 瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。
  • 系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。
  • GC操作有可能会影响到帧率,并使得用户感知到性能问题。

解决方案:

  • 避免在循环中创建临时对象
  • 避免在onDraw方法中创建Paint或Bitmap对象
  • 避免在onDraw方法中做耗时操作

4 卡顿

出现卡顿的原因:当界面刷新无法保证60fps时,导致丢帧的情况,给用户的感知就是卡顿。
出现卡顿的情况:

  • UI线程中的耗时操作,UI线程中有I/O读写、数据库访问等耗时操作
  • 复杂、不合理的布局以及OverDraw
  • 内存使用异常导致的卡顿
  • 错误的异步方式,涉及到线程优先级策略

解决方案:

  • StickMode:是一个类,需要嵌入到代码中。当违规操作发生时,可以根据自定义的策略记录下Log或者Crash,以便于跟踪改善。
  • TraceView:通过.trace文件,不仅能看出每个方法消耗的时间、发生次数,并且可以进行排序,直接从最耗时的方法开始处优化;
  • BlockCanary:自动监测卡顿的第三方工具,可以收集到卡顿的堆栈信息
    链接

5 ANR

5.1 ANR的分类:

  • KeyDispatchTimeout –按键或触摸事件在特定时间内无响应;
  • BroadcastTimeout –BroadcastReceiver在特定时间内无法处理完成;
  • ServiceTimeout –Service在特定的时间内无法处理完成;

5.2 ANR的发生原因:

  • 应用自身引起,如: 主线程阻塞、IOWait等;
  • 其他进程间接引起,如:
    ①当前应用进程进行进程间通信请求其他进程,其他进程的操作长时间没有反馈;
    ②其他进程的CPU占用率高,使得当前应用进程无法抢占到CPU时间片;

5.3 ANR日志分析:

当发生ANR的时候Logcat中会出现提示

04-06 15:58:46.215 23480-23483/com.example.testanr I/art: Thread[2,tid=23483,WaitingInMainSignalCatcherLoop,Thread*=0x7fa2307000,peer=0x12cb40a0,"Signal Catcher"]: reacting to signal 3
04-06 15:58:46.364 23480-23483/com.example.testanr I/art: Wrote stack traces to '/data/anr/traces.txt'

ANR的Log信息保存在:/data/anr/traces.txt,每一次新的ANR发生,会把之前的ANR信息覆盖掉。如:

04-01 13:12:11.572 I/InputDispatcher( 220): Application is not responding:Window{2b263310com.android.email/com.android.email.activity.SplitScreenActivitypaused=false}.
5009.8ms since event, 5009.5ms since waitstarted
04-0113:12:11.572 I/WindowManager( 220): Input event 
dispatching timedout sending 
tocom.android.email/com.android.email.activity.SplitScreenActivity

04-01 13:12:14.123 I/Process( 220): Sending signal. PID: 21404 SIG:3---发生ANR的时间和生成trace.txt的时间
04-01 13:12:14.123 I/dalvikvm(21404):threadid=4: reacting to signal 3 ……
04-0113:12:15.872 E/ActivityManager( 220): ANR in com.android.email(com.android.email/.activity.SplitScreenActivity)
04-0113:12:15.872 E/ActivityManager( 220): Reason:keyDispatchingTimedOut  -----ANR的类型
04-0113:12:15.872 E/ActivityManager( 220): Load: 8.68 / 8.37 / 8.53 --CPU的负载情况
04-0113:12:15.872 E/ActivityManager( 220): CPUusage from 4361ms to 699ms ago ----CPU在ANR发生前的使用情况;备注:这个ago,是发生前一段时间的使用情况,不是当前时间点的使用情况;

04-0113:12:15.872 E/ActivityManager( 220): 5.5%21404/com.android.email: 1.3% user + 4.1% kernel / faults:
10 minor
04-0113:12:15.872 E/ActivityManager( 220): 4.3%220/system_server: 2.7% user + 1.5% kernel / faults: 11
minor 2 major
04-0113:12:15.872 E/ActivityManager( 220): 0.9%52/spi_qsd.0: 0% user + 0.9% kernel
04-0113:12:15.872 E/ActivityManager( 220): 0.5%65/irq/170-cyttsp-: 0% user + 0.5% kernel
04-0113:12:15.872 E/ActivityManager( 220): 0.5%296/com.android.systemui: 0.5% user + 0% kernel
04-0113:12:15.872 E/ActivityManager( 220): 100%TOTAL: 4.8% user + 7.6% kernel + 87% iowait----注意这行:注意87%的iowait
04-0113:12:15.872 E/ActivityManager( 220): CPUusage from 3697ms to 4223ms later:-- ANR后CPU的使用量
04-0113:12:15.872 E/ActivityManager( 220): 25%21404/com.android.email: 25% user + 0% kernel / faults: 191 minor
04-0113:12:15.872 E/ActivityManager( 220): 16% 21603/__eas(par.hakan: 16% user + 0% kernel
04-0113:12:15.872 E/ActivityManager( 220): 7.2% 21406/GC: 7.2% user + 0% kernel
04-0113:12:15.872 E/ActivityManager( 220): 1.8% 21409/Compiler: 1.8% user + 0% kernel
04-0113:12:15.872 E/ActivityManager( 220): 5.5%220/system_server: 0% user + 5.5% kernel / faults: 1 minor
04-0113:12:15.872 E/ActivityManager( 220): 5.5% 263/InputDispatcher: 0% user + 5.5% kernel
04-0113:12:15.872 E/ActivityManager( 220): 32%TOTAL: 28% user + 3.7% kernel

从Logcat中可以得到以下信息:

  • 导致ANR的包名(com.android.emai),类名(com.android.email.activity.SplitScreenActivity),进程PID(21404)
  • 导致ANR的原因:keyDispatchingTimedOut
  • 系统中活跃进程的CPU占用率,关键的一句:100%TOTAL: 4.8% user + 7.6% kernel + 87% iowait;表示CPU占用满负荷了,其中绝大数是被iowait即I/O操作占用了。我们就可以大致得出是io操作导致的ANR。

注:并不是所有的ANR类型都有章可循,很多偶发的ANR受限于当时发生的环境或者系统Bug;因此对ANR,更应该强调预防而不是分析

5.4 ANR触发场景

①各大组件ANR

  • InputDispatching Timeout :输入事件分发超时5s未响应完毕;
  • BroadcastQueue Timeout :前台广播在10s内、后台广播在20秒内未执行完成;
  • Service Timeout :前台服务在20s内、后台服务在200秒内未执行完成;
  • ContentProvider Timeout :内容提供者,在publish过超时10s;

②CPU满负荷,I/O阻塞。解决办法:文件读写或数据库操作放在子线程异步操作。

③内存不足。解决办法:防止内存泄漏,优化内存使用

④主线程阻塞或主线程数据读取。解决办法:避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。

链接

6 网络优化

如果不正确的使用网络,可能带来一些问题,包括:

  • 流量耗费:过多以及没有经过处理的网络请求,会消耗用户的网络流量。
  • 电量消耗:与第一条流量消耗类似,耗电也会导致用户最终的卸载。
  • 用户体验差:网络请求耗时会给用户带来卡顿的产品体验,虽然可以使用Loading提升用户体验,但属于治标不治本。

解决方案:
网络优化主要从三个方面进行:1. 速度;2. 成功率;3. 流量。

  • Gzip压缩:HTTP协议上的Gzip编码是一种用来改进WEB应用程序性能的技术,用来减少传输数据量大小。即可以减少流量消耗、减少传输时间
  • IP直连与HttpDns:
    ①DNS解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,才用IP直连省去 DNS 解析过程,节省这部分时间。
    ②HttpDns:HttpDNS基于Http协议的域名解析,替代了基于DNS协议向运营商Local DNS发起解析请求的传统方式,可以避免Local DNS造成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。
  • 图片处理:
    ①通过webP格式可以使图片大小减少
    ②使用缩略图
    ③图片上传时使用分片传输,并设置重传机制
  • 协议层的优化:Http1.1版本引入了“持久连接”,多个请求被复用,无需重建TCP连接,而TCP连接在移动互联网的场景下成本很高,节省了时间与资源;
  • 合并多个请求:合并网络请求,减少请求次数。对于一些接口类如统计,无需实时上报,将统计信息保存在本地,然后根据策略统一上传。这样头信息仅需上传一次,减少了流量也节省了资源。
  • 网络缓存:对服务端返回数据进行缓存,设定有效时间,有效时间之内不走网络请求,减少流量消耗。
    链接

7 电量优化

电量优化的一般套路:

  • 在设置-电量里查看App的耗电情况;
  • 使用Battery Historian进行分析,这是分析里最重要的一步;
  • 针对分析结果,参照优化方式进行优化。
    详情见链接

8 Apk瘦身

8.1 代码瘦身

  • 移除无用代码、功能;
  • 移除无用的库、避免功能雷同的库;
  • 启用Proguard;
  • 缩减方法数;

8.2 资源瘦身

  • 移除无用的资源文件;
  • Drawable目录只保留一份资源;
  • 对图片进行压缩;
  • PNG转换JPG;
  • 使用矢量图;
  • 使用WebP;
  • 资源混淆;
  • 资源在线化;

8.3 So瘦身

针对用户机型分布保留特定架构的So;当前主流手机都是使用arm64-v8a(第 8 代、64 位 ARM 处理器),因此在实际开发中,进行如下配置即可:快速解决适配 64 位 App 的一个痛点

android {
      defaultConfig {
            ndk {
                    abiFilters 'armeabi-v7a', 'arm64-v8a'
            }
     }
}

链接

9 正确的异步姿势

参考

10 OOM

OOM可以大致归为以下3类:

  • 线程数太多
  • 打开文件太多
  • 内存不足

10.1 线程数太多

报错信息:pthread_create (1040KB stack) failed: Out of memory
解决思路:避免使用new Thread(),改用线程池

10.2 打开文件太多

报错信息:E/art: ashmem_create_region failed for 'indirect ref table': Too many open files Java.lang.OutOfMemoryError: Could not allocate JNI Env
解决思路:监控文件描述符、监控IO操作

10.3 内存不足

报错信息:


image.png

解决思路:避免内存泄露,做好内存优化,大图优化等

链接:https://juejin.cn/post/7074762489736478757

你可能感兴趣的:(性能优化篇)