背景
最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将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()把正常的主题替换回来。
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以及耗时,如:
参考链接
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 内存不足
报错信息:
解决思路:避免内存泄露,做好内存优化,大图优化等
链接:https://juejin.cn/post/7074762489736478757