本文章主要是对《App架构师实践指南》一书的阅读总结,作为自己阅读结果的提炼。
目录:
- 使用内部类最大的优点是什么
- 匿名内部类的内存泄露
- 如何在 github 上选择开源库
- 使用开源库时,为什么要封装一层
- 堆积、组件化、模块化以及插件化历程
- 重构分类
- App 质量监控思维导图
- CI 的概念
- Android 异常分类
- A/B 测试
- App 性能优化思维导图
- App 耗电优化
- 关于 16ms 与 60帧/s
- Android 中的内存泄露场景
- 包大小对下载转化率的影响
- 包大小优化方案
- App 冷启动速度量化方法
- App 冷启动优化方案
- Android 进程保活方案
- 关于 MultiDex 的一些点
- 关于 POM 依赖
1. 使用内部类最大的优点是什么
内部类可以非常好的解决多重继承的问题,每个内部类都能独立地继承一个 (接口的) 实现,所以无论外部类是否已经继承了某个(接口的) 实现,对于内部类都没有影响。
2. 匿名内部类的内存泄露
使用匿名内部类时,一定要慎重对待内存泄漏 (内部类保持了外部类的引用实例,内部类不销毁,外部类就无法被回收)。一般用静态内部类+弱引用方式或者动态代理方式替代。
3. 如何在 github 上选择开源库
- Author: 选择一个开源项目时,我们必须了解项目作者,是知名个人 (所谓网红) 还是大型公司 (如 Google 等),这是我们选择的依据之一。
- Last commit: 我们需要重点关注的是最后更新时间,如果该项目己经停止维护或者最后更新时间超过一年,就要慎重选择。
- 指标: Github 上,一个项目的 Star/Issues/PullRequests/Releases/Contributors/ Latest commit 信息值得我们关注。
- 文档: 可用于查看 README.md 、功能介绍、使用方法及基本原理等,便于快速集成验证。
- 依赖: 明确是否对其他第三方库有依赖,如果有很多依赖,则要谨慎使用。
- 聚合: 判断某项目是否是大而全的聚合型源码或框架?聚合型项目一般都是高藕合,很难扩展和业务适应,需谨慎使用。
4. 使用开源库时,为什么要封装一层
封装的好处非常多,如可以实现入口统一,适应业务变换或者开源项目本身的变换,灵活快速替换成其他开源库实现等。
5. 堆积、组件化、模块化以及插件化历程
Android 应用架构的发展,经历了原始野蛮式堆积、组件化、模块化以及插件化历程,这里我们谈谈 3 者的定义与异性。
- 模块化: 可以简单理解为:以业务功能为单元的独立模块,如登录模块化就是将登录模块抽离出来作为独立单元模块。
- 组件化: 组件化实现了与业务无关,以软件复用为核心,达到 "即插即用" 快速构造应用软件的效果。
- 插件化: 与组件化不同,插件化在运行时合并模块,而组件化是在编译时合并模块。插件化有黑科技的概念,它可以线上更换你手机应用中的代码或模块,实现远程控制。
6. 重构分类
重构的内容部分,分为架构和代码。
- 架构上: 随着业务的不断发展,当初的架构往往面临着各种问题,如无法满足客户的需求、无法实现应用的扩展、无法实现新的特性等,在这些情况下,作为架构师或开发者,将要开始考虑通过架构重构来解决问题。
- 代码上: 可能由于种种原因,先前代码存在结构混乱 (代码无层次堆积,各种代码风格杂交,强藕合等)、可读性差 (超长函数,代码不规范不 致,冗余代码,运算逻辑难以理解等) 等问题,在这些情况下,我们需要对代码进行重构。
7. App 质量监控思维导图
8. CI 的概念
持续集成 (Continuou Integration),英文缩写为 CI.
CI 词来源于极限编程 (Extreme Programming),作为它的 12 个实践之一出现,官方定义为 "持续集成是一种软件开发实践",即团队开发成员经常集成他们的工作,通常每个成员至少每天集成一次,也就是每天可能会发生多次集成,每次集成都通过自动化的构建 (包括编译、发布、自动化测试) 来验证,从而快速地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快地开发内聚的软件。
CI 的目的是让产品快速迭代,同时保持高质量,针对移动应用平台,可以简单地理解成当有人向代码库的主分支提交代码的时候,后台的持续集成服务器会尝试去构建整个产品,包括编译打包、自动化测试、质量分析等,输出结果成功或失败。
一个完整的 CI 流程如下图所示,包括开发者的代码提 CI Server Build 及测试,通过后再提交给 Code Server 合并,然后由 CI Server 打包给 QA(Quality Assurance),审核发布。
9. Android 异常分类
- Java 异常: Java 中出现未捕获异常,导致程序异常终止退出。
- ANR (Application Not Responding): 应用与用户进行交互时,在一定时间(如主线程输入事件中为 5 秒) 内没有响应用户的操作,则会引发 ANR 错误,并弹出一个系统提示框,让用户选择继续等待或立即关闭程序,同时会在/data/anr 目录下生成 traces.txt 文件,记录系统 ANR 异常的堆栈和线程信息。
- Native 异常: Native 异常/崩溃指在 Native 代码 C/C++ 中,因访问非法地址、地址对齐等问题,或程序主动 abort 所产生相应的 Signal 导致程序异常退出。Linux 中定义了很多 Signal ,当然并不是所有的 Signal 都会引发崩溃, 一般会引发异常退出的 Signal 有 SIGSEGV, SIGABRT, SIGILL, SIGBUS, SIGFPE。Native 异常具有与 Java 异常不同的特点:程序会直接闪退到系统桌面,Android 5.0 以下不会弹出提示框提醒程序崩溃,Android 5.0 以上会弹出提示框提醒程序崩溃。
10. A/B 测试
人生没有 AB 可选,但 App 是可以的。A/B 测试 (A/B Testing),简单来说,就是为目标制定两个方案 (比如两个页面),让部分用户使用 A 方案,另一部分用户使用 B 方案,记录下用户的使用情况,看哪个方案更符合设计目标。A/B Testing 是在移动 App 上验证产品方案的有力工具,可用于视觉 UI 选择、某个功能页面转换率判断等。例如,验证一个功能,方案 A 和方案 B 哪种用户更加接受和认可;再如,判断新功能的加入对产品各个指标的影响程度等。
11. App 性能优化思维导图
12. App 耗电优化
12.1 手机中的耗电大户/主要耗电场景
- 手机屏幕毋庸置疑,手机中最耗电的模块肯定是屏幕了。亮屏时间越长,电量消耗越快。
- CPU 相关复杂运算逻辑、无限循环等会直接导致 CPU 负载过高,耗电剧增。
- 网络相关。一般情况下,网络相关 (网络请求、数据传输、网络切换等) 是仅次于屏幕的耗电大户。例如网络请求,涉及通过内置的射频模块与基站通信,而射频模块又涉及一系列驱动和底层的支持,非常耗电,再如大数据的传输等。2009 Google 大会 Jeffrey Sharkey 的演讲中就总结了 Android 应用耗电主要在大数据传输、不停地网络间切换以及解析大文本数据个方面,而这些方面其实都是直接或间接地跟网络相关的。
- WakeLock 是 Android 系统中用于优化电量使用的一种手段,通过在用户一段时间没有操作的情况下让屏幕和 CPU 进入休眠状态来减少电量消耗。一些应用中出于特定业务场景调用 PowerManager. WakeLock 使 CPU 保持持续运转,而释放需要时间,甚至你根本就忘记释放了,灭屏后 CPU 却还一直运转,从而大大增加了耗电量。
- GPS 定位涉及 GPS 位置传感器,也是 位不折不扣的耗电大户 。平时不使GPS 的时候,记得把它给关了。
- Camera 涉及前后摄像头硬件,如果一直使用 (录屏等),耗电也会非常可观。
12.2 电量优化手段
网络相关:
- 发起网络请求时机。业务区分当前网络请求是需要及时返回结果的 (用户主动下拉刷新等),还是可以延迟执行的 (异步上传数据),可以延迟执行的有针对性地把请求行为绑定在一起发出。
- 减少移动网络被激活的时间和次数。采用回退机制来避免固定频繁的同步请求,例如,在发现返回数据相同的情况下,推迟下次的请求时间。使用 Batching (批处理) 的方式来集中发出请求,避免频繁的间隔请求,例如同一业务尽量少使用多次请求,合并多次请求。使用 Prefetching (预取) 的技术提前把一些数据拿到,避免后面频繁再次发起网络请求。
- 数据处理,网络数据传输前进行压缩处理,进行大数据量下载时,尽量使用 GZIP 方式下载,使用高效率的数据格式和解析方法,推荐使用 JSON Protobuf.
- 慎用或禁用 Polling (轮询) 的方式去执行网络请求, Android 可以采用 Google Cloud Messageing, iOS 可以采用 APNs.
- 减少推送消息次数和频率。App 收到服务端大量或频繁的推送消息,对手机的耗电肯定会有一定影响。
- 网络状态,处理具体业务前,养成判断当前网络状态的习惯和编程思维。例如,在移动网络下,减少数据传输或降低数据传输频率,Wi-Fi 下网络传输耗电远比移动网络少。在网络不可用状态下,尽早进入网络异常处理逻辑,避免不必要的运算逻辑等。
界面相关:
- 离开某个界面后停止对应的耗电活动。例如,用户离开了界面,而对应的耗电活动井没有及时停止,就会造成资源浪费。
- 应用进入后台禁止异常消耗电量。
定位相关:
- 使用 GPS 后记得及时关闭,减少更新频率,根据实际情况切换 GPS 和网络,不要任何时候都同时使用两者。
- 对定位要求不高的业务场景,尽量用网络定位代替 GPS。
- 慎用持续定位,对于大多数场景,使用一次定位接口即可。
- 慎用被动定位,防止被动定位唤醒。
消息广播,程序中避免频繁地监昕系统广播或业务消息造成严重耗电问题,灵活控制消息广播接收的有效与无效状态。
Android 专栏:
- 慎用 WakeLock,使用 WakeLock 时一定记得成双成对,及时释放,使用 WakeLock 时,建议通过带参数的 aquire 设置超时,以防止 App 异常等不可抗拒因素导致没有释放。
- 定时任务选择 Android 中可以通过 Handler/Timer、AlarmManager 以及 JobSchedule (Android 5.0+) 3 种方式执行定时任务,前台任务建议使用 Handler/Timer ,简单直观;后台任务,对调度时机没有强烈要求的场景,建议使用 JobSchedule
管理任务 (Android 5.0+),对于触发时间准确性要求非常高的场景,如果没法通过算法降级处理,再考虑 AlarmManager ,对于 WAKEUP 类型且 Exact 调度模式的 AlarmManager 任务一定要慎用。
13. 关于 16ms 与 60帧/s
- 绘制原理 16ms 原则:Android 系统每隔 16ms 发出 VSync 信号,触发对 U1 进行渲染,这就意味着 Android 系统要求每一帧都要在 16ms 这个时间内绘制完成,即无论代码或业务如何复杂,要保证平滑完成一帧,那么渲染代码必须在 16ms 内完成,从而保证流畅的用户体验,这个速度意味着要能够达到流畅的画面需要 16ms 的帧率。
- 60 帧/s 与 16 ms:为什么会以 60 帧/s 或 16 ms 为标准呢?其实两者是一个统一的概念,一帧 16ms,即 1/0.016 帧/s=62.5帧/s ,而 60 帧/s的标准是源于人眼和大脑之间的协作无法感知超过 60 帧/s 的画面更新。市场上绝大多数 Android 设备的屏幕刷新频率都 60Hz ,超过 60 帧/s 就没有实际意义。
14. Android 中的内存泄露场景
14.1 长时间保持对 Activity Context View Drawable 其他对象的引用
- Activity 使用静态成员,建议使用静态的 Activity View 等。
- Context 处理 Thread、第三方库初始化等异步程序时,这些异步程序的生命周期可能大于 Activity 的生命周期,导致 Activity 无法被回收,造成内存泄露。
- 建议与 View 无关的操作, Context 尽量使用 Application Context.
- Activity 被弱引用包裹时,虽然 GC 时会回收弱引用持有的对象,但是如果弱引用它本身持有的 Activity 没有销毁,此时也不会被回收。
14.2 内部类
当非静态内部类中使用静态实例时,因为每个非静态内部类会持有个外部类的隐式引用,这可能会导致不必要的问题。我们尽量使用静态内部类代替非静态内部类,并通过弱引用存储一些必要的生命周期引用。
14.3 匿名类
与非静态内部类类似,持有外部类的引用导致内存泄露。
14.4 持有对象的时间超出需要的时间/引用对象没有释放 (注意持有对象的生命周期)
- register 对象后缺少对应的 unregister 操作,如广播等。
- 集合对象未清理,资源对象未关闭。如 cursor File 等资源。
- static 滥用,static 用于修饰大内存占用对象时,会导致该对象无法回收,造成内存泄露。
- bitmap 使用完后没回收。
15. 包大小对下载转化率的影响
App 包大小优化会对我们的业务产生哪些影响呢?
通过 App 瘦身来提高我们 App 的下载转化率,这是具体业务运营指标,通俗一点理解就是 App 包越小,用户下载等待时间越短,更适应低存储容量配置的手机,应用下载转化率也就越高。
16. 包大小优化方案
Android APK 由以下几部分组成,分别为 classes.dex, resources.arsc, res, assets, lib 及其他资源 (AndroidManifest, project.properties, proguard.cfg 和 META-INF),我们就直接讲解重点,从 APK 组成进行分类优化阐述。
16.1 classes.dex 源码
- 代码混淆,在 build.gradle 中开启 minifyEnable,进行 Proguard 混淆。
- 删除无用代码,使用 Android Studio Inspect Code 和 Code Cleanup 进行静态代码检查,删掉无用代码。
- 第三方库/jar, 删除无用库,合并功能重复的库,选择更小的库。
- 代码优化。
16.2 resources.arsc
resources.arsc 存放的是编译后的二进制文件,以 id-name-value 式存储 map.
- 使用 Android Studio Inspect Code 删掉不必要的资源 ID.
- 使用 Google 的 android-arscblamer 工具检查删除不必要的资源映射,如部分空引用。
16.3 res
该文件夹是包大小优化大户,存放诸如音视频、图片等多媒体资源,需重点关注。
【1】删除无用资源
- build.gradle 中开启 shrinkResources ,不打包未使用的资源。
- build.gradle 中通过 resConfigs 配置业务所需的语言资源,去除无用语言资源。
- 借助 Android Studio 分析 Unused Resource ,去掉无用 res。
- 使用 Lint 工具扫描去除无用资源。
【2】适当使用图片压缩
- 使用图片压缩相关工具对图片进行有损压缩。
- 不考虑透明度业务下可以用 JPG 图片代替 PNG 图片,例如背景页、启动页等。
- 尝试使用 WebP 格式代替 PNG 格式,但注意必须是 Android 4.0 以上系统,若需要兼容 Android 4.0 以下系统,需要引入额外的兼容库,可能得不偿失,且目前 Android Studio 并不支持 WebP 布局文件的预览。
- 对大的图片资源进行缩放处理,尽可能只保存 份图片资源 (建议放在 xhdpi 文件夹)。
【3】适当使用音视频压缩,采用有损格式 (Ogg、MP3, AAC WMA Opus 等)。
【4】资源混淆,集成 AndResGuar 等工具对资源进行优化处理。
【5】使用 Drawable XML Color .9.PNG 代替 PNG 图片,例如渐变背景、纯色背景等。
16.4 assets 文件
- 利用 FontZip 等工具对字体进行提取优化,删除无用字体。
- 减少 icon -font 使用,使用 svg 代替 icon-font。
- 资源网络化,动态下载,如字体、表情包、贴纸等。
- 考虑对资源文件进行压缩储存,代码中进行解压缩获取,例如 H5 页面。
16.5 lib 库文件
- 在 build.gradle 中使用 abiFilters 按需配置 CPU 架构 (如 armeabi-v7a, x86, armeabi, x86-64 等),移除不需要兼容的 so 文件。
- 使用更小的库或合并现有库(如 C++ 运行时库统一使用 stlport_shared)。
17. App 冷启动速度量化方法
17.1 方法 1: adb shell 方式
命令为 adb shell am start -W [pkg_name]/[activity],如下为微信第一次启动时间信息,会有 个时间信息,分别如下。
- ThisTime。一般和 TotalTime 相同,除非在应用启动时开了一个透明的 Activity 等,预先处理后再显示主 Activity ,这样 Total Time 要小,其表示一连串启动 Activity 到最后一个 Activity 启动耗时。
- TotalTime。新应用启动耗时,包括新进程启动+Application 初始化+Activity 启动的时间,这是开发者一般要关注的真正启动耗时间。
- WaitTime (Android 5.0+)。总的耗时,包括新应用启动耗时以及前一个应用 Activity pause 时间。
17.2 方法二:logcat 方式 (Android 4.4+)
Android 4.4 之后, Android 在系统 Log 中添加了 Display Log 信息,可以通过过滤 ActivityManager Display 关键字,抓 logcat 中的启动时间信息。命令为 adb logcat I grep "ActivityManager"。
17.3 方法 3: TraceView 工具
我们在 UI 和 CPU 性能优化中介绍了 TraceView ,其可以完整地显示每个函数/方法的时间消耗,有两种使用方式。
- 直接通过 DDMS start traceview 启动,弹窗选择 trace 模式开始记录。
- 代码集成方式,在需要调试的地方加入 Debug.startMethodTracing("xx"),在结束的地方加入 Debug.stopMethodTracing(),运行后将生成 XX.trace 文件,然后通过 DDMS 打开该 trace 文件即可分析,注意需要 "android.permission.WRITE_EXTERNAL STORAGE" 权限。
18. App 冷启动优化方案
App 启动速度优化,也称 App 快启,主要从减少耗时和优化体验两个部分进行优化即可。
18.1 减少耗时
【1】Application 减轻繁重的 App 初始化,除非立即需要的,其他对象都采取延迟初始化/懒初始化,全局静态对象放到一个单例中懒初始化;在构造方法、attachBaseContext()、onCreate() 中不做耗时操作;一些数据预取放在异步线程/后台任务中等。
【2】Activity 减轻繁重的 Activity 初始化
- 避免大量复杂布局,尽量减少布局的层次和嵌套布局。
- 不必要在启动时展示的 view 可以通过 ViewStub 实现,需要时再填充。
- 避免加载或编码 bitmap ,那些依赖 bitmap view 延迟更新。
- 避免硬盘或网络操作阻塞主 UI 绘制。
- 避免在主线程/UI 线程中进行资源初始化操作,可以延迟初始化或者在子程中去做。
【3】大型 APP 开发 App 初始化组件
其核心就是对所有的初始化任务进行分类分级,各个任务并行处理,同时设置预显示内容,这样各个业务初始化模块互不依赖,且不影响 App 快启,也不会因为新增业务初始化而造成不必要的工作量。
18.2 优化体验
我们可以通过主体化 App 启动屏幕来改善启动体验, 一种常用的方式是在等待第一帧的时间里,加入一些配置以增加体验(Android Material Design 中建议使用 placeholder UI), 如加入 Activity windowBackground 题属性来为启动的 Activity 提供一个简单的 drawable (例如设置成我们的 App logo 或者透明色等),这个背景会在显示第一帧前提前显示在界面上。
19. Android 进程保活方案
20. 关于 MultiDex 的一些点
- MultiDex 即多 DEX 实现,其 APK 解压缩后会有多个 DEX 文件,如 classes.dex classes2.dex 等,每个 DEX 可以最大承载 64K 方法。
- 65536 的关键字,其代表的是单个 Dalvik Executable (DEX) 字节码文件内的代码可调用的引用总数,官方将其称为 "64K 引用限制"。
- 可能遇到的问题 MultiDex 虽贵为官方方案,但使用中存在 些大大小小的问题,如影响应用的启动时间, ANRCrash 等。其主要原因是 MultiDex.install 需要在主线程 中执行,当 secondary.dex 过大时,加载超过 5s 就产生了 ANR.
- Andorid 5.0 之前,系统只加载一个主 dex,其它的 dex 采用 MultiDex 手段来加载;Andorid 5.0 之后,ART 虚拟机天然支持MultiDex,Android 5.0 (API 级别 21)及更高版本使用名为 ART 的运行时,后者原生支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex 文件,并将它们编译成单个 .oat 文件,供 Android 设备执行。因此,如果你的 minSdkVersion 为 21 或更高值,则不需要 Dalvik 可执行文件分包支持库。
21. 关于 POM 依赖
POM 依赖发布到 maven 仓库时,会带上 POM 文件。POM 依赖会存在依赖传递,比如 A -> B -> C,会带上这些依赖关系。如果是非 POM 依赖,那么 A 依赖的库将不会带上,集成到宿主中时,如果宿主中没有 B, C ,则运行时会崩溃。POM 依赖的好处是能保证当前库的完整性,但是会出现其依赖库的版本冲突问题。非 POM 依赖不会存在冲突问题,但是如果宿主中不存在其子依赖库,或者版本不对,也极其危险。