Android高手开发课之高质量开发 学习笔记

2021-04-09  本文已影响0人  _明川
image

极客课程地址

主要总结 用两周的时间浅显的过了一遍shwen大佬的高手开发课 高质量开发章节内容,以便后续继续学习使用

1. native崩溃

Android 崩溃分为 Java 崩溃Native 崩溃

Java 崩溃 : 是在 Java 代码中,出现了未捕获异常,导致程序异常退出。

Native 崩溃 : 一般都是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 abort,这些都会产生相应的 signal 信号,导致程序异常退出。

崩溃捕捉流程


崩溃捕捉流程

崩溃捕捉难点:

  1. 文件句柄泄漏,导致创建日志文件失败 (提前申请文件句柄 fd 预留)
  2. 栈溢出了,导致日志生成失败 (signalstack)
  3. 堆的内存都耗尽了,导致日志生成失败 (Breakpad)
  4. 堆破坏或二次崩溃导致日志生成失败 (Breakpad)

使用Breakpad 捕捉Native异常 :链接 Github lib 工具类

崩溃分析

崩溃现场:

  1. 崩溃信息 (进程名 线程名 崩溃堆栈 )
  2. 系统信息 (logcat 机型 系统 厂商 cpu等信息)
  3. 内存信息 (使用内存 剩余内存)

崩溃尝试解决:

  1. 查找可能存在的问题 (根据logcat 日志 等信息定位)
  2. 尝试规避 (try catch 、避免使用某一个api)
  3. Hook 解决 (例如 Android 7.0 Toast失效)

2. 内存优化

内存造成的问题:

  1. 异常 (OOM、内存分配失败)
  2. 卡顿 (java内存不足引发的频繁GC gc的时候程序会有卡顿 、系统负载过高)

举例子:
Android 3.0到8.0 关于Bitmap优化的演变,从开始的数据放native bitmap放java内存,到都放Java内存,演变到8.0则都放在Native层。

如何将Bitmap定义在Native层

eg:


// 步骤一:申请一张空的 Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);

// 步骤二:申请一张普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);

// 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);

// 步骤四:释放 Java Bitmap 内存
srcBitmap.recycle();
srcBitmap = null;

存在的问题:1.版本兼容问题 2频繁申请释放Java Bitmap

测量内存

adb shell dumpsys meminfo <package_name|pid> [-d]

脱离 Android Studio,实现一个自定义的“Allocation Tracker”

原理:
项目使用了 inline hook 来拦截内存对象分配时候的 RecordAllocation 函数,通过拦截该接口可以快速获取到当时分配对象的类名和分配的内存大小。
在初始化的时候我们设置了一个分配对象数量的最大值,如果从 start 开始对象分配数量超过最大值就会触发内存 dump,然后清空 alloc 对象列表,重新计算。

内存优化

  1. 设备分级 (可参考device-year-class 对设备进行分级 ,根据设备环境分配内存)
  2. Bitmap优化 (从Java内存迁移到Native、图片尺寸/质量压缩、超容器大图处理、重复图片优化、Bitmap内存及时回收)
  3. 缓存管理 (统一缓存管理机制 负责各个模块大小)
  4. 进程优化 (一个空进程也会占用很多内存,避免多余进程的使用)
  5. 内存泄漏 (LeakCanary 检测对象生命周期是否异常、游标释放、强引用持有对象回收、Handler 弱引用 、WebView单独进程维护)
  6. 资源优化 (压缩图片、Svg图片类型使用、so库优化、语言文件优化等)

启动优化

app启动四个状态

  1. 预览窗口显示 :系统在拉起app进程之前,会先根据app的 Theme 属性创建预览窗口。当然如果我们禁用预览窗口或者将预览窗口指定为透明,用户在这段时间依然看到的是桌面。
  2. 闪屏显示: 在app进程和闪屏窗口页面创建完毕,并且完成一系列 inflate view、onmeasure、onlayout 等,可以看到欢迎界面了
  3. 主页显示: 在完成主窗口创建和页面显示的准备工作后,用户可以看到app的主界面,对应 onCreate;
  4. 界面可操作性 : 走到 onResume 方法,用户即可进行操作

优化方式

  1. 闪屏优化 (欢迎界面 设置默认背景)
  2. 业务梳理 (避免Applcation集体初始化,可以维护进程池用于不重要模块的懒加载)
  3. 线程优化 (避免多个任务等待执行,通过有向无环图 设置加载顺序 减少 CPU 调度带来的波动,让应用的启动时间更加稳定)
  4. 安装包不压缩 (启动过程需要的文件,我们可以指定在安装包中不压缩,这样也会加快启动速度,但带来的影响是安装包体积增大)

更黑科技点的优化方式

  1. I/O 优化 (磁盘I/O优化 sp文件过大,db过大 都需要优化,启动过程不建议出现网络 I/O)
  2. 数据/类/资源 重排 (ReDex 调整dex排序 、支付宝资源重排优化

I/O优化

CPU 和内存相比磁盘是高速设备,整个流程的瓶颈在于磁盘 I/O 的性能

文件系统

image
  1. 虚拟文件系统(VFS)。它主要用于实现屏蔽具体的文件系统,为应用程序的操作提供一个统一的接口。
  2. 文件系统(File System)。ext4、F2FS 都是具体文件系统实现,文件元数据如何组织、目录和索引结构如何设计、怎么分配和清理数据,
  3. 页缓存(Page Cache)。在读文件的时候会,先看它是不是已经在 Page Cache 中,如果命中就不会去读取磁盘。

磁盘

image

在一些低端机上面,大量跟 I/O 相关的卡顿原因:

  1. 内存不足。当手机内存不足的时候,系统会回收 Page Cache 和 Buffer Cache 的内存,大部分的写操作会直接落盘,导致性能低下
  2. 写入放大。内存重复写入需要先进行擦除操作,但这个擦除操作的基本单元是 block 块,一个 page 页的写入操作将会引起整个块数据的迁移,这就是典型的写入放大现象
  3. 由于低端机的 CPU 和闪存的性能相对也较差,在高负载的情况下容易出现瓶颈

系统为了缓解磁盘碎片问题,可以引入 fstrim/TRIM 机制,在锁屏、充电等一些时机会触发磁盘碎片整理。

mmap
它是通过把文件映射到进程的地址空间,带来的好处:

  1. 减少系统调用。我们只需要一次 mmap() 系统调用,后续所有的调用像操作内存一样,而不会出现大量的 read/write 系统调用
  2. 减少数据拷贝。普通的 read() 调用,数据需要经过两次拷贝;而 mmap 只需要从磁盘拷贝一次就可以了,并且由于做过内存映射,也不需要再拷贝回用户空间。

缺点:

  1. 虚拟内存增大
  2. 磁盘延迟

I/O跟踪

  1. Java Hook (找到切入点 。缺点:性能差 没办法监控Native代码 每个版本源码可能不同 都需要兼容)
  2. Native Hook (暂时了解)

I/O优化

  1. 对大文件使用 mmap 或者 NIO 方式 (MappedByteBuffer就是 Java NIO 中的 mmap 封装)
  2. Buffer 复用 (Okio开源库,它内部的 ByteString 和 Buffer 通过重用等技巧,很大程度上减少 CPU 和内存的消耗)
  3. 存储结构和算法的优化

存储优化

SharedPreferences 存在的问题

  1. 跨进程不安全 (由于没有使用跨进程的锁,就算使用MODE_MULTI_PROCESS 也会丢失。)
  2. 加载缓慢。(sp 文件的加载使用了异步线程,而且加载线程并没有设置线程优先级。)
  3. 全量写入 (无论是调用 commit() 还是 apply(),即使我们只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次)
  4. 卡顿 (由于提供了异步落盘的 apply 机制,在崩溃或者其他异常情况可能会导致数据丢失。当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 sp 对象数据落地到磁盘。如果没有落地完成,这时候主线程会被一直阻塞)

MMKV 的实现原理,里面有一些非常不错的思路。例如利用文件锁保证跨进程的安全、使用 mmap 保证数据不会丢失、选用性能和存储空间更好的 Protocol Buffer 代替 XML、支持增量更新等

ContentProvider

ContentProvider 的生命周期默认在 Application onCreate() 之前,而且都是在主线程创建的,尽量不做过多操作,通过 ContentProvider 也可以获取到数据,比如系统提供的 日历、通讯录等等

序列化

  1. Serializable

  Java原生序列化机制 缺点:大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还会递归序列化对象引用的其他对象。

  我们能做的优化:1. writeObject 和 readObject 方法,Serializable在反射的时候会先检查是否自己实现了 writeObj 和 readObj . 2 writeReplace 和 readResolve 方法,可以自定义序列化返回结果,

read/writeResolve  read/writeObject 顺序 区别
// 序列化
E/test:SerializableTestData writeReplace
E/test:SerializableTestData writeObject

// 反序列化
E/test:SerializableTestData readObject
E/test:SerializableTestData readResolve

Serializable 注意: 类的 static 变量以及被声明为 transient 的字段,默认的序列化机制都会忽略该字段。 开发中尽量设置 serialVersionUID

  2. Parcelable

在写入和读取的时候都需要手动添加自定义代码,使用起来相比 Serializable 会复杂很多。但Parcelable 不需要采用反射的方式去实现序列化和反序列化

  3. Json

优点:速度更快,体积更小,结果可读便于排查问题。使用方便,支持跨平台、跨语言,支持嵌套引用

  4. Protocol Buffers

由于使用二进制格式,相比Json 体积更小,传输更快。

  5. SQLite

  SQLite本身是支持多线程 多进程操作的,通过文件锁来控制多进程的并发。SQLite 锁的粒度精确到db,没有到某一行。

  多进程可以同时获取 SHARED 锁来读取数据,但是只有一个进程可以获取 EXCLUSIVE 锁来写数据库。

  多线程情况下可以使用连接池,或者使用WAL模式,将读和写完全的并发执行。
  通过正确的建立索引,可以提升 SQLite 的查询速度。通过调整默认的页大小和缓存大小,可以提升 SQLite 的整体性能。

索引参考文章
wcdb github

网络优化

网络优化大头在网络请求:

  1. DNS 解析 (通过 DNS 服务器,拿到对应域名的 IP 地址 优化点:HTTPDNS 百度DNS优化)
  2. 创建连接 (服务器建立连接,这里包括 TCP 三次握手、TLS 密钥协商等工作 优化点:连接复用,通过将连接存放连接池和Http2.0的多路复用 或者Http1.0的 keep-alive 都对连接有优化作用)
  3. 发送 / 接收数据 (如何根据网络状况将带宽利用好,怎么样快速地侦测到网络延时,在弱网络下如何调整包大小 优化点:Http2.0 头部压缩 和 请求Gzip压缩 算法压缩等方式)
  4. 关闭连接 (从主动关闭和被动关闭两种情况优化)

UI优化

关于屏幕适配可以参考限宽适配今日头条UI适配

优化点:

  1. 尽量使用硬件加速 (Gpu可以分担cpu渲染工作量提升速度)
  2. View的异步加载 /重用 (子线程Looper替换为UI线程Looper 用完记得还原 。重用可以参考 ListView 和 RecycleView复用原理)
  3. measure/layout 优化 (减少UI层级 推荐使用ConstraintLayout 代替Rl 和Ll ,避免重复设置背景)
  4. Compose 学习和使用 (声明式UI布局 让界面更加简单 减少xml解析时间 ,ui的未来)

安装包优化

  1. 代码方面 可以使用 ProGuard对代码进行压缩简化,但是对混淆规则 要避免重复keep 。更高级的牵扯到dex的压缩 和 Native Lib的分割和合并 参考:redex github Lib合并参考
  2. 资源压缩可以参考 AndResGuard AndResGuard美团讲解 和 腾讯的 matrix思路(官方提供: Lint 和 shrinkResources)
上一篇下一篇

猜你喜欢

热点阅读