天天看点

分享篇 -《App架构师实践指南》阅读总结

本文章主要是对《App架构师实践指南》一书的阅读总结,作为自己阅读结果的提炼。

目录:

  1. 使用内部类最大的优点是什么
  2. 匿名内部类的内存泄露
  3. 如何在 github 上选择开源库
  4. 使用开源库时,为什么要封装一层
  5. 堆积、组件化、模块化以及插件化历程
  6. 重构分类
  7. App 质量监控思维导图
  8. CI 的概念
  9. Android 异常分类
  10. A/B 测试
  11. App 性能优化思维导图
  12. App 耗电优化
  13. 关于 16ms 与 60帧/s
  14. Android 中的内存泄露场景
  15. 包大小对下载转化率的影响
  16. 包大小优化方案
  17. App 冷启动速度量化方法
  18. App 冷启动优化方案
  19. Android 进程保活方案
  20. 关于 MultiDex 的一些点
  21. 关于 POM 依赖
分享篇 -《App架构师实践指南》阅读总结

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 质量监控思维导图

分享篇 -《App架构师实践指南》阅读总结

8. CI 的概念

持续集成 (Continuou Integration),英文缩写为 CI. 

CI 词来源于极限编程 (Extreme Programming),作为它的 12 个实践之一出现,官方定义为 "持续集成是一种软件开发实践",即团队开发成员经常集成他们的工作,通常每个成员至少每天集成一次,也就是每天可能会发生多次集成,每次集成都通过自动化的构建 (包括编译、发布、自动化测试) 来验证,从而快速地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快地开发内聚的软件。 

CI 的目的是让产品快速迭代,同时保持高质量,针对移动应用平台,可以简单地理解成当有人向代码库的主分支提交代码的时候,后台的持续集成服务器会尝试去构建整个产品,包括编译打包、自动化测试、质量分析等,输出结果成功或失败。

一个完整的 CI 流程如下图所示,包括开发者的代码提 CI Server Build 及测试,通过后再提交给 Code Server 合并,然后由 CI Server 打包给 QA(Quality Assurance),审核发布。

分享篇 -《App架构师实践指南》阅读总结

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 性能优化思维导图

分享篇 -《App架构师实践指南》阅读总结
分享篇 -《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 进程保活方案

分享篇 -《App架构师实践指南》阅读总结

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 依赖不会存在冲突问题,但是如果宿主中不存在其子依赖库,或者版本不对,也极其危险。