02.Android性能优化总结

参考链接:Android性能优化总结

前言

在Android应用优化方面,我们主要从以下4个方面进行优化:

  • 稳定(内存溢出、崩溃)
  • 流畅(卡顿)
  • 耗损(耗电、流量、网络)
  • 安装包(APK瘦身)

1.稳定优化

01.内存优化

由于Android应用的沙箱机制,每个应用所分配的内存大小是有限度的,内存太低就会触发LMK(Low Memory Killer)机制,进而会出现闪退现象。
在Android应用开发中,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。
其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用。所以做好Crash监控,把崩溃信息、异常信息收集记录起来,以便后续分析;合理使用主线程处理业务,不要在主线程中做耗时操作,防止ANR程序无响应发生。

- 优化内存的意义

  1. 减少 OOM, 提高应用稳定性
  2. 减少卡顿,提高应用流畅度
  3. 减少内存占用,提高应用后台存活
  4. 减少异常发生,减少代码逻辑隐患

- 内存检测工具

  1. MAT
  2. LeakCanary

- 内存泄漏

  • 定义:

    当这个对象不需要再使用,应该完整的执行最后的生命周期,但是由于某些原因,对象虽然已经不再使用,仍然在内存中并没有结束整个生命周期,这就意味着对象已经泄漏了。

  • 内存泄漏场景

    1. 资源型对象未关闭: Cursor,File
    2. 注册对象未销毁: 广播,回调监听
    3. 类的静态变量持有大数据对象
    4. 非静态内部类的静态实例
    5. Handler 临时性内存泄漏: 使用静态 + 弱引用,退出即销毁
    6. 容器中的对象没清理造成的内存泄漏
    7. WebView: 使用单独进程

- 内存抖动

一般指在很短的时间内发生了多次的内存分配和释放。会直接造成应用卡顿。

- 内存优化方案

1. AutoBoxing(自动装箱)

能用 int 坚决不用 Integer

2. 内存复用

  1. 有效利用系统自带的资源
  2. 视图复用(ViewHolder)
  3. 对象池
  4. Bitmap 对象复用: 利用 Bitmap 的 inBitmap 的特性。

3. 使用最优的数据类型

  1. HashMap 与 ArrayMap 的选型

    当对象的数目在 1K 以内,但是访问特别多,或者删除和插入频率不高时可以使用 ArrayMap

4. 枚举类型:

使用注解枚举限制替换 Enum

5. 图片内存优化

  1. 选择合适的位图格式

    通过设置 options.inPreferredConfig = RGB_8888 来决定解码格式

    RGB_8888 :4byte

    RGB_565:2byte

    ARGB_4444: 透明 16 byte

    ALPHA_8:透明 1byte

  2. 建议 Bitmap 优化代码

    1. Bitmap 内存占用计算

      **加载资源文件计算方式: **

      长 * (设备 dpi / 资源目录对应的 dpi) * 宽 * (设备 dpi / 资源目录对应的 dpi) * 位图格式 = /1024/1024 = ? MB

      其它:

      w * h * 位图格式

    BitmapFactory.Options options = new BitmapFactory.Options();
    //不分配内存大小,和不返回实际 bitmap
    option.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(is,null,options);
    //isScaled = true: 系统会按照现有的目标密度来重新划分目标密度
    options.isScaled = true;
    //生成对应的大小
    options.inDensity = options.outWidth;
    //设置缩放比--> 处理采样为原始图片的 1/4 大
    options.inSampleSize = 4;
    //生成对应的大小
    option.inTargetDensity = dstWith*options.inSampleSize;
    //需要分配内存大小,返回实际 bitmap
    option.inJustDecodeBounds = false;
    BitmapFactory.decodeStream(is,null,options);
    
  3. 图片多级缓存设计

6. static final

基本数据类型如果不用修改的建议全部写成 static final,因为 它不需要进行初始化工作,直接打包到 dex 就可以直接使用,并不会在 类 中进行申请内存

7. 拼接优化

字符串拼接别用 +=,使用 StringBuffer 或 StringBuilder

8. 重复申请内存问题

  • 同一个方法多次调用,如递归函数,回调函数中 new 对象
  • 不要在 onMeause, onLayout, onDraw 中去刷新 UI

9. 图片格式优化

drawable 中的 png,jpg 图片转换为 webp 格式图片

10. 色彩格式转换

不要用 Java 代码转换 RGB 格式或者转 Bitamp 资源。特占内存资源

02. 提高代码质量

  1. 团队之前可以相互代码审查
  2. 使用 Link 扫表代码,是否有缺陷性。

03. Crash

  1. 通过实现 Thread.UncaughtExceptionHandler 接口来全局监控异常状态,发生 Crash 及时上传日志给后台,并且及时通过插件包修复。
  2. native 捕获
    1. 线上可以使用腾讯的 Bugly 框架
    2. 局域网内开发可以使用Google 开源的 breakpad 框架

04. ANR

  • 产生 ANR 原因
    1. 主线程堵塞,死循环
    2. 死锁
    3. 频繁大量 GC
    4. 当前应用程序抢占 CPU 时间片失败

05. 提高后台存活

  1. Activity 提权:监控手机锁屏解锁事件,在屏幕锁屏时启动 1 个像素透明的 Activity ,在用户解锁时将 Activity 销毁掉,从而达到提高进程优先级的作用。
  2. Service 提权 :SDK >= 26 通过 startForegroundService 启动一个前台服务,如果开启 startForegroundService 前台服务,那么必须在 5 s内开启一个前台进程的服务通知栏,不然会报 ANR
  3. 广播拉活:在发生特定系统事件时,系统会发出广播,通过在 AndroidManifest 中静态注册对应的广播监听器,即可在发生响应事件时拉活。但是从android 7.0 开始,对广播进行了限制,而且在 8.0 更加严格。
  4. 全家桶拉活
  5. Service 机制拉活
    将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活
    只要 targetSdkVersion 不小于5,就默认是 START_STICKY。
    但是某些 ROM 系统不会拉活。并且经过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,一旦在短时间内 Service 被杀死 4-5 次,则系统不再拉起。
  6. 账号同步拉活(只做了解,不靠谱)
  7. JobScheduler 拉活(靠谱,8.0 官方推荐)
    JobScheduler 允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果即开启一个定时器,与普通定时器不同的是其调度由系统完成。
    注意 setPeriodic 方法
    在 7.0 以上如果设置小于 15 min 不起作用,可以使用setMinimumLatency 设置延时启动,并且轮询
  8. 推送拉活:根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送。
  9. Native 拉活:Native fork 子进程用于观察当前 app 主进程的存亡状态。对于 5.0以上成功率极低。
  10. 后台循环播放一条无声文件,比较耗电
  11. 双进程守护 (靠谱)
  12. 加入白名单电量优化

总结: Activity + Service 提权 + Service 机制拉活 + JobScheduler 定时检测进程是否运行 + 后台播放无声文件 + 双进程守护可以组成一个进程保活终极方案。

2.交互优化

交互是与用户体验最直接的方面,交互场景大概可以分为四个部分:UI 绘制、应用启动、页面跳转、事件响应。对于上面四个方面,大致可以从以下两个方面来进行优化:

  • 界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。
  • 数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

对此,我们可以从一下几个方面优化:

1. 布局优化

01. 布局优化必备分析工具

  1. Hierarchy Viewer: 用来检查 Layout 层级关系
  2. 检查是否过度绘制工具:手机设置->开发者选项->打开 GPU 过度绘制开关

02. 优化方案

  • 减少层级

    1. 合理使用 RelativeLayout 与 LinearLayout 布局。
    2. 合理使用 Merge 布局标签
    3. 如果布局层级确实很多,可以不使用 setContentView 直接使用系统的 content 布局 ID 最后直接 addView(View v) 也行。
  • 提高显示速度

    1. ViewStub

      应用场景: 当某个布局当中的子 View布局非常多,但并不是所有元素都同时显示出来,而是 二选一或者 N 选一,打开布局在选择。那么这个时候就可以考虑时候该标签,而不是使用 VISIBLE 或 INVISIBLE 属性。

  • 布局复用

    如果多个 xml 布局中都会使用相同的布局对象,那么可以考虑把相同的布局抽出一个单独的布局文件,然后以 include 形式添加

  • 如何避免过度绘制

    1. 布局上的优化

      1. 移除 XML 中非必须的背景,或根据条件设置。
      2. 移除 Window 默认的背景。
      3. 按需显示占位背景图片
    2. 自定义 View 上的优化:如果有覆盖绘制的情景,应该把覆盖的区域裁剪。

03.总结:

  1. 布局的层级越少,加载速度越快。
  2. 减少同一层控件的数量,加载速度会变快。
  3. 一个控件的属性越少,解析越快。
  4. 尽量多使用 RelativeLayout 与 LinearLayout 布局。
  5. 将可复用的组件抽取出来,在需要使用的地方通过 include 标签加载。
  6. 使用 ViewStub 标签加载不常用的布局
  7. 使用 merge 标签减少布局的嵌套层级
  8. 尽可能少用 wrap_content ,wrap_content 会增加布局 measure 时的计算成本,已知宽高为固定值时,不要使用 wrap_content 属性
  9. 删除控件中无用属性

2. 渲染优化

  • 布局上的优化,移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片。
  • 自定义View优化,使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。

3. 启动优化

应用一般都有闪屏页,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。也可以通过启动加载逻辑优化。可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。数据准备。数据初始化分析,加载数据可以考虑用线程初始化等策略。

- 查看耗时

  1. LogCat 过滤 Displayed 可以查看 Activity 启动时间
  2. 保存 trace 文件,查看具体耗时函数启动时间
//开始计时
Debug.startMethodTracing(filePath);
  中间为需要统计执行时间的代码
//停止计时
Debug.stopMethodTracing();
  1. 通过 systrace 查看耗时

    1. 命令输入 systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a 包名 -o test.log.html
    2. chrom 输入 chrome://tracing/ 点击 load 加载 html
    3. 只看当前进程

- 黑白屏

原因:

  • 系统 AppTheme 主题 设置了windowBackground

优化:

  1. 在自己的 AppTheme 加入 windowBackgroup。
  2. 设置 windowbackgroup android:windowIsTranslucent 透明 。
  3. 为第一个SplashActivity 单独设置一个主题,相当于加入广告页 。

- 启动优化方案

1. Application 启动优化

  1. Application 生命周期中如果需要延迟初始化的,对异步要求不高可以选择开子线程。
  2. 懒加载,用的时候才初始化
  3. 改用 IntentService onHandleIntent 加载耗时任务

2. 线程优化

  1. 控制线程数据量使用线程池
  2. 检查线程之间锁机制,是否相互过于依赖

3. GC 优化

  1. 避免进行大量的字符串操作,特别是序列化和反序列化
  2. 频繁创建的对象需要考虑复用

4. 主页面启动优化建议

  1. 布局减少层级关系
  2. onCreate 中不要做耗时任务
  3. 使用 ViewStub 、include 、merge 标签
  4. 闲时调用,通过 IdleHandler 来实现主线程闲时监控,用于加载一些不那么重要的资源

5. APK 瘦身

  1. 代码资源混淆
  2. 减少 dex 数量

6. redex 重排列 class 文件

redex 是 Facebook 开源的一款字节码优化工具,目前只支持 mac 和 linux。

通过文件重排列的目的,将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 Linux 文件系统的 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。

冷启动优化方案 5.0 以下:

在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。

4. 刷新优化

  • 减少刷新次数;
  • 缩小刷新区域;

5. 动画优化

  1. 尽量别用补间动画,改为属性动画,因为通过性能监控发现补间动画重绘非常频繁
  2. 使用硬件加速提高渲染速度,实现平滑的动画效果。

3.耗电优化

在 Android5.0 以前,在应用中测试电量消耗比较麻烦,也不准确,5.0 之后专门引入了一个获取设备上电量消耗信息的 API,即Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,和Systrace 一样,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。

优化建议

1. 加入电量白名单

2. GPS 优化

  1. 选择合适的定位模式
  2. 选择合适的定位间隔
  3. 不用及时注销

3. 文件上传

不是紧急的文件可以选择用户连接 wifi 在上传,同时配个一定的规则,避免用户一直使用 4G 环境(可以选择如果在多少时间内网络状态还是 wifi 并且在充电状态下可以选择上传大文件,比如 APP 日志等)

4. 慎用 WakeLock 唤醒 CPU

  1. 亮屏替换
//在Activity中: 
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

//或在布局中添加这个属性:
android:keepScreenOn="true"

  1. alarm 闹钟让 CPU 间断式工作

5. JobScheduler (8.0 后 Google 推荐使用)

  1. 把工作任务放到合适的时间再去执行,比如充电时间,wifi 连接后
  2. 可以把多个任务合并到一起,再选择时间去执行
  3. 在充电并且连接 wifi 的状态下发送数据(这里旋转屏幕是为了发送数据用的)

6. 减少 View 绘制,借鉴 布局优化

7. 复杂计算尽量使用 native 处理

8. TCP 心跳机制建议 30s 以后

9. 定时器任务如果不是特殊的也尽量在 30s 以后

4.网络优化

对于网络的优化,可以从以下几个方面着手进行:

01. 图片网络优化

例如,针对网络情况,返回不同的图片数据,一种是高清大图,一种是正常图片,一种是缩略小图。当用户处于wifi下给控件设置高清大图,当4g或者3g模式下加载正常图片,当弱网条件下加载缩略图。

02. 网络数据优化

  • 连接复用:节省连接建立时间,如开启 keep-alive。
  • 对于Android来说默认情况下HttpURLConnection和HttpClient都开启了keep-alive。只是2.2之前HttpURLConnection存在影响连接池的Bug,具体可见:Android HttpURLConnection及HttpClient选择
  • 请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。如果某个页面内请求过多,也可以考虑做一定的请求合并。
  • 减少请求数据的大小:对于post请求,body可以做gzip压缩的,header也可以做数据压缩。返回数据的body也可以做gzip压缩,body数据体积可以缩小到原来的30%左右。
  • 减小返回数据大小
    1. 使用 Gzip 压缩
    2. 精简数据格式
    3. 对于不同的设备不同网络返回不同的内容 如不同分辨率图片大小
    4. 需要数据更新时,可考虑增量更新。如常见的服务端进行 bsdiff,客户端进行 bspatch。
    5. 支持断点续传,并缓存 Http Resonse 的 ETag 标识,下次请求时带上,从而确定是否数据改变过,未改变则直接返回 304
    6. 缓存获取到的数据,在一定的有效时间内再次请求可以直接从缓存读取数据。

03. 异常拦截优化

在获取数据的流程中,访问接口和解析数据时都有可能会出错,我们可以通过拦截器在这两层拦截错误。

  • 在访问接口时,我们不用设置拦截器,因为一旦出现错误,Retrofit会自动抛出异常。比如,常见请求异常404,500,503等等。
  • 在解析数据时,我们设置一个拦截器,判断Result里面的code是否为成功,如果不成功,则要根据与服务器约定好的错误码来抛出对应的异常。比如,token失效,禁用同账号登陆多台设备,缺少参数,参数传递异常等等。

5.APK瘦身

应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。
对此,我们可以从以下几个方面着手

  • 代码混淆。使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
  • 资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。
  • 图片优化。比如利用 AAPT 工具对 PNG 格式的图片做压缩处理,降低图片色彩位数等。
  • 避免重复功能的库,使用 WebP图片格式等。
  • 插件化,比如功能模块放在服务器上,按需下载,可以减少安装包大小。

6.存储优化

- SharedPreferences

性能问题

  1. 当 SharePreferences 文件还没有被加载内存时,调用 getSharedPreferences 方法会初始化文件并读入内存,这容易导致耗时更长
  2. Editor 的 commit 或者 apply 方法每次执行时,同步写入磁盘耗时较长。

优化建议

  1. 使用 apply 异步写入
  2. SharedPreferences 类中的 commitToMember() 方法会锁定 SharedPreferences 对象,Put 和 getEditor 方法会锁定 Editor 对象,在写入磁盘时更会锁定一个写入锁,因此要避免频繁的读写 SharedPreferences ,减少无畏的调用。
  3. 对于 SharedPreferences 的批量操作,最好先获取一个 editor ,进行批量操作,然后调用 apply 方法。这样会比 commit 方法性能高

- SQLite

  1. 使用事务
    beginTransaction
    setTransactionSuccessful
    endTransaction
  2. 使用索引
  3. 异步线程

一图总结

posted @ 2022-02-10 15:24  契阔  阅读(404)  评论(0编辑  收藏  举报