- 崩溃优化:关于“崩溃”那些事儿
- Android 的两种崩溃
- java 崩溃: 在 Java 代码中,出现了未捕获异常,导致程序异常出。
- Native 崩溃: 一般是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 abort(中止),这些都会产生相应的 signal 信号,导致程序异常退出。
- Native 崩溃
- Native 崩溃的捕获流程
- 编译端。编译 C/C++ 代码时,需要将带符号信息的文件保留下来。
- 客户端。捕获到崩溃时候,将收集到尽可能多的有用信息写入日志文件,然后选择合适的时机上传到服务器。
- 服务端。读取客户端上报的日志文件,寻找适合的符号文件,生成可读的 C/C++ 调用栈。
- Native 崩溃捕获的难点: 在上面的三个流程中,最核心的是怎么样保证客户端在各种极端情况下依然可以生成崩溃日志。在崩溃时,程序会处于一个不安全的状态,如果处理不当,非常容易发生二次崩溃。
- 情况一:文件句柄泄漏,导致创建日志文件失败,怎么办?
应对方式:我们需要提前申请文件句柄 fd (标识) 预留,防止出现这种情况。 - 情况二:因为栈溢出了,导致日志生成失败,怎么办?
应对方式:为了防止栈溢出导致进程没有空间创建调用栈执行处理函数,我们通常会使用常见的 signalstack。在一些特殊情况,我们可能还需要直接替换当前栈,所以这里也需要在堆中预留部分空间。 - 情况三:整个堆的内存都耗尽了,导致日志生成失败,怎么办?
应对方式:这个时候我们无法安全地分配内存,也不敢使用 stl 或者 libc 的函数,因为它们内部实现会分配堆内存。这个时候如果继续分配内存,会导致出现堆破坏或者二次崩溃的情况。Breakpad(监测工具) 做的比较彻底,重新封装了Linux Syscall Support,来避免直接调用 libc。 - 情况四:堆破坏或二次崩溃导致日志生成失败,怎么办?
应对方式:Breakpad 会从原进程 fork 出子进程去收集崩溃现场,此外涉及与 Java 相关的,一般也会用子进程去操作。这样即使出现二次崩溃,只是这部分的信息丢失,我们的父进程后面还可以继续获取其他的信息。在一些特殊的情况,我们还可能需要从子进程 fork 出孙进程。
- 情况一:文件句柄泄漏,导致创建日志文件失败,怎么办?
- 选择合适的崩溃服务
- 从产品化跟社区维护来说,Bugly 在国内做的最好。
- 从技术深度跟捕获能力来说,阿里 UC 浏览器内核团队打造的啄木鸟平台最佳。
- 还有 Google 的 Firebase 等等。
- 友盟(项目使用)
- Native 崩溃的捕获流程
- 崩溃分析
- 第一步:确定重点,在日志中找到重要的信息,对问题有一个大致判断。
- 确认严重程度。 优先解决 Top 崩溃或者对业务有重大影响,例如启动、支付过程的崩溃。
- 崩溃基本信息。 确定崩溃的类型以及异常描述,对崩溃有大致的判断。
- Java 崩溃。Java 崩溃类型比较明显,这个时候需要去进一步查看日志中的 “内存信息”和“资源信息”。
- Native 崩溃。需要观察 signal、code、fault addr 等内容,以及崩溃时 Java 的堆栈。
- ANR。先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是由于大量 GC 导致卡死。
- Logcat。 Logcat 一般会存在一些有价值的线索,日志级别是 Warning、Error 的需要特别注意。从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,例如出现 ANR 时,会有“am_anr”;App 被杀时,会有“am_kill”。不同的系统、厂商输出的日志有所差别,当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。
-各个资源情况。 结合崩溃的基本信息,我们接着看看是不是跟 “内存信息” 有关,是不是跟“资源信息”有关。比如是物理内存不足、虚拟内存不足,还是文件句柄 fd 泄漏了。
- 第二步:查找共性,如果使用了上面的方法还是不能有效定位问题,我们可以尝试查找这类崩溃有没有什么共性。找到了共性,也就可以进一步找到差异,离解决问题也就更进一步。
- 第三步:尝试复现,为了进一步确认更多信息,就需要尝试复现崩溃。如果我们对崩溃完全没有头绪,也希望通过用户操作路径来尝试重现,然后再去分析崩溃原因。
- 查找可能的原因。 通过上面的共性归类,我们先看看是某个系统版本的问题,还是某个厂商特定 ROM 的问题。
- 尝试规避。 查看可疑的代码调用,是否使用了不恰当的 API,是否可以更换其他的实现方式规避。
- 第一步:确定重点,在日志中找到重要的信息,对问题有一个大致判断。
- Android 的两种崩溃
- 内存优化
- 两个问题
- 异常
- OOM
- 内存分配失败
- 因为整体内存不足导致应用被杀死、设备重启等
- 卡顿
- Java 内存不足会导致频繁 GC,这个问题在 Dalvik 虚拟机会更加明显。而 ART 虚拟机在内存管理跟回收策略上都做大量优化,内存分配和 GC 效率相比提升了 5~10 倍。
- 物理内存不足时系统会触发 low memory killer 机制,系统负载过高是造成卡顿的另外一个原因。
- bitmap优化
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22); // 2. 申请一张普通的 Java Bitmap Bitmap srcBitmap = BitmapFactory.decodeResource(res, id); // 3. 使用 Java Bitmap 将内容绘制到 Native Bitmap 中 mNativeCanvas.setBitmap(nativeBitmap); mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint); // 4. 释放 Java Bitmap 内存 srcBitmap.recycle(); srcBitmap = null;
- 统一图片库。 图片内存优化的前提是收拢图片的调用,这样我们可以做整体的控制策略。可以使用 Glide、Fresco 或者采取自研都可以,而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。
- 异常
- 卡顿优化
- 可以把预览窗口实现成闪屏的效果,这样用户只需要很短的时间就可以看到“预览闪屏”。
- 线程数量太多会相互竞争 CPU 资源,因此要有统一的线程池,并且根据机器性能来控制数量。线程切换的数据
- 根据业务优先显示业务部分
- 使用最新控件展示界面 列表等
- 不要在主线程进行网络访问/大文件的IO操作
- 绘制UI时,尽量减少绘制UI层次;减少不必要的view嵌套 在view层级相同的情况下,尽量使用 LinerLayout而不是RelativeLayout;因为RelativeLayout在测量的时候会测量二次,而LinerLayout测量一次
- 删除控件中无用的属性
- 合理的刷新机制,尽量减少刷新次数,尽量避免后台有高的 CPU 线程运行,缩小刷新区域
- 内存泄漏
- 资源使用完未关闭
- 广播(BraodcastReceiver)动态注册之后要反注册,推荐在onStart onStop 对应的生命周期执行
- 服务(Service)Start 之后 记得 Stop。启动服务时机看需求。一般不建议在 Application 启动(启动 Service 耗时基本要100ms+)
- io Cursor 流要记得 close,一定要在 finally 去 close,防止抛异常没执行 close ,那就泄漏了
- Bitmap 内存大户,要记得回收 recycle 一下,当然 90% 的场景 Glide 已经帮我们处理的
- 单利泄露
主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放。例如 :activity 的 content 赋值到单利对象里面的成员量变量
- 资源使用完未关闭
- 两个问题
private static volatile ClassXX instance;
private Context context;
private ClassXX(Context context) { t
his.context = context;
}
public static ClassXX getInstance(Context context) {
if (instance == null) { s
ynchronized (instance) {
if(instance == null) {
instance = new ClassXX(context);
}
}
} return instance;
}
如果这个Context是 Activity 的 Context ,当你的 Activity finish(); 之后Activity 这个对象的内存还是在堆中,没有释放。
因为单利对象持有Activity 的引用,jvm 认为你这个对象还是在使用中,不敢去 回收掉你的 Activity。那单例什么时候被回收?那就只有等到整个进程被回收了,单例才会被回收。
进程杀死(回收):
Process.killProcess(Process.myPid())
用户手动卡片式摧毁 (亲测可行)
解决方法:
传入和单例一样生命周期的对象,如context.getApplication();
不将 context保存在单例的成员变量里面
-
APP包优化方案
- 资源优化
- 少用几套图
- 图片资源进行压缩
- 代码优化
- 实现模块化 功能化
- 无用的代码删除
- 删除无用的依赖
- 使用优秀的库
- 代码混淆
- 实现配置压缩资源
- 资源优化
-
存储优化
-
常见的数据存储方法
存储就是把特定的数据结构转化成可以被记录和还原的格式,这个数据格式可以是二进制的,也可以是 XML、JSON、Protocol Buffer 这些格式。- 第一,SharedPreferences 的使用。
SharedPreferences 是 Android 中比较常用的存储方法,它可以用来存储一些比较 小的键值对集合。
虽然 SharedPreferences 使用非常简便,但也是我们诟病比较多的存储方法。它的性能问题比较多,我可以轻松地说出它的“七宗罪”。- 跨进程不安全。由于没有使用跨进程的锁,就算使用MODE_MULTI_PROCESS,SharedPreferences 在跨进程频繁读写有可能导致数据全部丢失。根据线上统计,SP 大约会有万分之一的损坏率。
- 加载缓慢。SharedPreferences 文件的加载使用了异步线程,而且加载线程并没有设置线程优先级,如果这个时候主线程读取数据就需要等待文件加载线程的结束。
这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50~100ms,我建议提前用异步线程预加载启动过程用到的 SP 文件。 - 全量写入。无论是调用 commit() 还是 apply(),即使我们只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是性能差的重要原因之一。
- 卡顿。由于提供了异步落盘的 apply 机制,在崩溃或者其他一些异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 SharedPreferences 对象数据落地到磁盘。如果没有落地完成,这时候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看 SP 卡顿占比一般会超过 5%。
系统提供的 SharedPreferences 的应用场景是用来存储一些非常简单、轻量的数据。我们不要使用它来存储过于复杂的数据,例如 HTML、JSON 等。而且 SharedPreference 的文件存储性能与文件大小相关,每个 SP 文件不能过大,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来。
我们也可以替换通过复写 Application 的 getSharedPreferences 方法替换系统默认实现,比如优化卡顿、合并多次 apply 操作、支持跨进程操作等。具体如何替换呢?在今天的 Sample 中我也提供了一个简单替换实现。- ContentProvider 的使用
为什么 Android 系统不把 SharedPreferences 设计成跨进程安全的呢?那是因为 Android 系统更希望我们在这个场景选择使用 ContentProvider 作为存储方式。ContentProvider 作为 Android 四大组件中的一种,为我们提供了不同进程甚至是不同应用程序之间共享数据的机制。
Android 系统中比如相册、日历、音频、视频、通讯录等模块都提供了 ContentProvider 的访问支持。它的使用十分简单- 启动性能
ContentProvider 的生命周期默认在 Application onCreate() 之前,而且都是在主线程创建的。我们自定义的 ContentProvider 类的构造函数、静态代码块、onCreate 函数都尽量不要做耗时的操作,会拖慢启动速度。 - 稳定性
ContentProvider 在进行跨进程数据传递时,利用了 Android 的 Binder 和匿名共享内存机制。简单来说,就是通过 Binder 传递 CursorWindow 对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。 - 安全性
虽然 ContentProvider 为应用程序之间的数据共享提供了很好的安全机制,但是如果 ContentProvider 是 exported,当支持执行 SQL 语句时就需要注意 SQL 注入的问题。另外如果我们传入的参数是一个文件路径,然后返回文件的内容,这个时候也要校验合法性,不然整个应用的私有数据都有可能被别人拿到,在 intent 传递参数的时候可能经常会犯这个错误。
- 启动性能
- 第一,SharedPreferences 的使用。
总的来说,ContentProvider 这套方案实现相对比较笨重,适合传输大的数据。
-
数据库的优化
- 使用 StringBuilder 代替 String
- 查询时返回更少的结果集及更少的字段查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间及内存,更多的字段会导致更多的内存消耗
- Android 中数据不多时表查询可能耗时不多,不会导致 ANR,不过大于 100ms 时同样会让用户感觉到延时和卡顿,可以放在线程中运行,但 sqlite 在并发方面存在局限,多线程控制较麻烦,这时候可使用单线程池,在任务中执行 db 操作,通过 handler 返回结果和 UI 线程交互,既不会影响 UI 线程,同时也能防止并发带来的异常。
- SQLiteOpenHelper 维持一个单例
因为 SQLite 对多线程的支持并不是很完善,如果两个线程同时操作数据库,因为数据库被另一个线程占用, 这种情况下会报“Database is locked” 的异常。所以在数据库管理类中使用单例模式,就可以保证无论在哪个线程中获取数据库对象,都是同一个。
最好的方法是所有的数据库操作统一到同一个线程队列管理,而业务层使用缓存同步,这样可以完全避免多线程操作数据库导致的不同步和死锁问题。 - Application 中初始化
使用 Application 的 Context 创建数据库,在 Application 生命周期结束时再关闭。
在应用启动过程中最先初始化完数据库,避免进入应用后再初始化导致相关操作时间变长。 - 索引
索引就像书本的目录,目录可以快速找到所在页数,数据库中索引可以帮助快速找到数据,而不用全表扫描,合适的索引可以大大提高数据库查询的效率。
优点:大大加快了数据库检索的速度,包括对单表查询、连表查询、分组查询、排序查询。经常是一到两个数量级的性能提升,且随着数据数量级增长。
缺点:索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。
在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。
-
对象的序列化
应用程序中的对象存储在内存中,如果我们想把对象存储下来或者在网络上传输,这个时候就需要用到对象的序列化和反序列化。
对象序列化就是把一个 Object 对象所有的信息表示成一个字节序列,这包括 Class 信息、继承关系信息、访问权限、变量类型以及数值信息等。
1. Serializable
Serializable 是 Java 原生的序列化机制,在 Android 中也有被广泛使用。我们可以通过 Serializable 将对象持久化存储,也可以通过 Bundle 传递 Serializable 的序列化数据。
2. Serializable 的原理
Serializable 的原理是通过 ObjectInputStream 和 ObjectOutputStream 来实现的,我们以 Android 6.0 的源码为例,你可以看到
整个序列化过程使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化对象引用的其他对象。
整个过程计算非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。 -
数据的序列化
json :
JSON 是一种轻量级的数据交互格式,它被广泛使用在网络传输中,很多应用与服务端的通信都是使用 JSON 格式进行交互。
JSON 的确有很多得天独厚的优势,主要有:- 相比对象序列化方案,速度更快,体积更小。
- 相比二进制的序列化方案,结果可读,易于排查问题。
- 使用方便,支持跨平台、跨语言,支持嵌套引用。
因为每个应用基本都会用到 JSON,所以每个大厂也基本都有自己的“轮子”。例如 Android 自带的 JSON 库、Google 的Gson 阿里fastjson 美团Mson
各个自研的 JSON 方案主要在下面两个方面进行优化:- 便利性。例如支持 JSON 转换成 JavaBean 对象,支持注解,支持更多的数据类型等。
- 性能。减少反射,减少序列化过程内存与 CPU 的使用,特别是在数据量比较大或者嵌套层级比较深的时候效果会比较明显。
- 在数据量比较少的时候,系统自带的 JSON 库还稍微有一些优势。但在数据量大了之后,差距逐渐被拉开。总的来说,Gson 的兼容性最好,一般情况下它的性能与 Fastjson 相当。但是在数据量极大的时候,Fastjson 的性能更好。
-