第24讲:APK如何做到包体积优化?
因为每个项目的背景、实现方式都不尽相同,导致各个项目之间能列出的共性相对较少。本节主要从两部分谈谈对包体积优化的一些尝试:安装包监控、安装包大小优化。
安装包监控
Android Studio 的APK Analyser
analyser的使用非常简单,只需将需要分析的apk文件拖入Android Studio即可,它会显示各项内容所占的大小,并按照大小排序,比如图片占用了比较大的资源空间,可以针对其做压缩优化等操作。
Matrix 的ApkChecker
ApkChecker是腾讯开源框架Matrix的一部分,主要是用来对Android安装包进行分析检测,并输出较为详细的检测结果报告。正常情况下我们需要下载Matrix源码,并单独编译Matrix-apk-canary部分。但如果想快速使用ApkChecker,可以直接在网上下载其ApkChecker.jar文件,然后创建一个配置文件.json即可。
ApkChecker的好处是可以命令行使用,这样可以很方便将其在自动化集成系统中,并对最终生成的apk文件进行分析,将产出报告发送到指定位置。
安装包优化实践
删除无用文件
使用Lint查看未引用资源。它能识别出项目中没有被任何代码所引用到的资源。也可以使用shrinkResources能够在项目编译,删除所有在项目中未被使用到的资源文件。但是需要将minifyEnabled选项设置为true。
使用resConfigs限定国际化资源文件。有时候我们使用的三方库中可能对各种国际化语言进行支持,但是我们自己的项目只支持某个语言,比如中文,那么我们可以在gradle的defaultConfig中使用resConfigs来限制打包到apk中的国际化资源文件,实例如下:
defaultConfig {
...
//限定支持的语言
resConfigs "zh"
}
文件优化
- 关于静态图片优化 优先使用VectorDrawable图片,如果UI无法提供VectorDrawable图片,那么webp格式是一个不错的选择。Android Studio也支持将png或者jpg格式转化为webp格式,如图:
- 关于动态图片优化 实际上webp也可以作为动态图,只是目前对webp动图支持的三方库不多,谷歌官方的glide对webp支持也不是很友好。
- 关于引入三方库 我们在引入一个完整的三方库的时候,很可能只需要其中的一部分功能,所以整个库的引用显得性价比不高,因此,需要我们将使用的那一部分代码从三方库中提取出来放到项目中。
- 关于App Bundle 如果是海外App项目,会舒服很多,因为谷歌官方支持动态发布。正常情况下我们的apk中为了更好地适配屏幕、语言等,会在项目中添加多套相应的资源文件,比如不同hdpi的drawable,或者不同CPU下的so文件,最终打包生成的apk中会包含所有的资源文件。但实际上一台设备只会用到这其中的一套资源,这无形中就已经产生了一些不必要的资源浪费。而谷歌的Dynamic Delivery 功能就天然第解决了这个问题,通过Google Play Store安装apk时,会根据安装设备的属性,只选择相应的资源打包到apk文件里。
另外,我们在项目中也使用了另一个APP Bundle中比较好用的选项--Dynamic Asset Delivery。这个功能本来只是针对安装包超过100M的App,但是不影响我们使用这套方案进行安装包优化。具体做法就是讲大部分assets中的资源使用无损压缩的方式,压缩成一个.obb格式的文件,然后每次发布apk时都将次obb文件设置为apk的Bundle文件,这样也可以减少用户实际安装包大小。但是APP Bundle 目前只适合在Google Store上发布的项目,国内目前还是通过各家的插件化方案来实现动态部署,一定程度上可以算减少安装包大小的方案。
总结:本课主要讲了项目中关于安装包优化的一些实践,主要分为2方面:
- 安装包的监控 利用Apk Analyzer和ApkChecker工具来分析安装包大小,开发过程中养成良好的编程习惯以及code review习惯。
- 安装包优化实践 主要思路就是,删除无用资源或代码,并对资源进行相应的压缩优化。实际上除了资源文件,对于代码部分也可以更进一步的优化,比如使用Proguard,或者直接使用R8编译方式。只是因为R8还处于试验阶段,项目中没有过多实践。
第25讲:Android 崩溃那些事儿
Android系统输出的crash日志可以分为2类:JVM异常堆栈信息、Native代码崩溃日志。JVM异常堆栈信息又分为两种:检查异常和非检查异常。所谓检查异常就是在代码编译时期,Android Studio就会提示错误,无法通过编译,比如IOException;非检查异常包括error和运行时异常(RuntimeException),AS不会在编译期间提示这些异常信息,而是在程序运行时期因为代码错误而直接导致崩溃,比如OOM或者空指针异常(NPE)。
Java异常
针对java异常,我们都可以使用UncaughtExceptionHandler来进行捕获操作,它是Thread的一个内部接口,我们可以自定义实现UncaughtExceptionHanlder接口,并实现uncaughtException方法:
public class CustomExceptionHandler implements Thread.UncaughtExceptonHandler{
private Thread.UncaugthExceptionHanlder mDefaultHanlder;
public void init(Context context){
mContext = context;
//系统默认UncaughtExceptionHandler
mDefaultHanlder = Thread.getDefaultUncaughtExceptionHandler();
//设置该CrashHandler为系统默认的
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread , Throwable ex){
if(!handleException(ex) && mDefaultHanlder != null){
//如果自己没处理,交给系统处理
mDefaultHanlder.uncaughtException(thread,ex);
}else{
//自己处理
Intent intent = new Intent(mContext , CrashDisplayActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
android.os.Process.killProcess(android.os.Process.myPid());
}
}
}
需要注意的几点:
- 在自定义异常处理类中需要持有线程默认异常处理类,这样做的目的是在自定义异常处理类无法处理或者处理异常失败时,还可以将异常交给系统做默认处理。
- 如果自定义异常处理类成功处理异常,需要进行页面跳转,或者将程序”杀死“。否则程序会一直卡死在崩溃页面,并弹出无响应对话框。
一般情况下,在handleException方法中需要做以下几件事情:
- 收集crash现场的相关信息,如当前APP版本信息、以及手机设备的相关信息等;
- 日志的记录工作,将收集到的信息及系统抛出的异常信息保存到本地,比如以文件的方式保存。
- 使用的时候,在Application的onCreate方法中调用CustomExceptionHandler.getInstance().init(this);
native异常
当程序中的native代码发生崩溃时,系统会在/data/tombstones目录下保存一份详细的崩溃日志信息。如果一个native crash 是必现的,不妨在模拟器上重现bug,并将/data/tombstones中的崩溃日志拉到本地电脑加以分析。
但如果native crash是偶发现象,并且在模拟器上一时难以复现,那就需要一种机制,将native crash现场的日志信息保存到我们可以访问的手机目录中。目前比较成熟、使用也比较广泛的就是谷歌的BreakPad。BreakPad是个跨平台的开源库,我们也可以在其Breakpad GitHub上下载自己编译,并通过JNI的方式引入到项目中。
线上崩溃日志获取
目前比较成熟、采用也比较多的是腾讯的Bugly,来满足线上版本捕获crash的所有需求,包括Java层和native层的crash都可以获取相应的日志。并且Bugly每天都会邮件通知上一天的崩溃日志,方便测试和开发统计bug的分布以及崩溃率。
除了Bugly之外,还有一些其他的crash上报工具。比如XCrash和Sentry。这两者比Bugly好的地方就是除了自动拦截界面崩溃事件,还可以主动上报错误信息。XCrash的使用比较灵活,可以通过设置不同的过滤方式,针对性的上报相应的crash日志,并且在捕获crash之后,可以加入自定义的操作,比如本地保存日志或者直接进行网络上传。另外,Sentry可以通过设置过滤,来判断是否上报crash日志,这对于SDK的开发人员是很有用的。比如SDK开发商只是想收集自身SDK引入的crash,对于用户的其他操作导致的crash进行过滤,这种情况下就可以考虑集成Sentry。
总结:Android的崩溃分为两类,Java层和Native层。针对Java层一般通过自定义UncaughtExceptionHandler进行异常拦截;针对Native层可以考虑集成谷歌的breakpad进行捕获,并保存日志到本地。
第26讲:面对内存泄漏,如何进行有优化?
内存泄漏是个隐形炸弹,其本身并不会造成程序异常,但是随着量的增长会导致其他各种并发症:OOM、UI卡顿等。
Activity内存泄漏预防
Activity承担了与用户交互的职责,因此内部需要持有大量的资源引用以及系统交互的Context,这会导致一个Activity对象的retained size特别大。一旦Activity因为被外部系统所持有而发生内存泄漏,被牵连导致其他对象的内存泄漏也会非常多。
造成Activity内存泄漏的场景主要有以下几种情况:
- 将Context或者View置为static;
View默认会持有一个Context的引用,如果将其置为static将会造成View在方法区中无法被快速回收,最终导致Activity内存泄漏。public class ActivityA extend AppCompatActivity{ private static ImageView imageView; ... }
imageView会导致ActivityA无法被GC回收。
- 未解注册各种Listener,比如广播;
- 非静态Handler导致Activity泄漏;
- 三方库使用Context或单例使用的Context不当;
在项目中使用各种三方库,有些初始化的时候需要传入一个Context对象,但其内部有可能一直持有此Context引用,比如将其设置为static,这样如果外部传入的Context是Activity,就会导致外部Activity的隐形泄漏,一般在SDK开发中,尽量使用外部传入的Context的getApplicationContext方法获取全局上下文,而不要直接使用外部传入的Context。
内存泄漏检测
LeakCanary是Square公司的一个开源库,通过它可以在APP运行过程中检测内存泄漏,当内存泄漏发生时,会生成泄漏对象的引用链,并通知程序开发人员。
LeakCanary主要分2大核心部分:
- 如何检测内存泄漏;
- 分析内存泄漏对象的引用链;
如何检测内存泄漏
Java中的WeakReference是弱引用类型,每当发生GC时,它所持有的对象如果没有被其他强引用所持有,那么它引用的对象就会被回收。
WeakReference的构造函数可以传入ReferenceQueue,当WeakReference指向的对象被垃圾回收器会收时,会把WeakReference放入ReferenceQueue中。如下代码,调用WeakReference的构造器时,传入一个自定义的ReferenceQueue:
public class WeakRefDemo{
public static void main(String[] args) throws InterruptedException{
ReferenceQueue<BigObject> queue = new ReferenceQueue<>();
WeakReference<BigObject> reference = new WeakReference<>(new BigObject(),queue);
System.out.println("before gc,reference.get is"+reference.get());
System.out.println("before gc,queue is"+queue.poll());
System.gc();
Thread.sleep(1000);
System.out.println("after gc,reference.get is"+reference.get());
System.out.println("after gc,queue is"+queue.poll());
}
static class BigObject{}
}
那么打印结果如下:
before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
before gc, queue is null
after gc, reference.get is null
after gc, queue is java.lang.ref.WeakReference@4e25154f
可以看出,当BigObject被回收后,WeakReference会被添加到所传入的ReferenceQueue中。再修改一下上述代码,模拟一个内存泄漏,如下:
public class WeakRefDemo{
public static void main(String[] args) throws InterruptedException{
ReferenceQueue<BigObject> queue = new ReferenceQueue<>();
BigObject bigObject = new BigObject();
WeakReference<BigObject> reference = new WeakReference<>(bigObject, queue);
System.out.println("before gc,reference.get is"+reference.get());
System.out.println("before gc,queue is"+queue.poll());
System.gc();
Thread.sleep(1000);
System.out.println("after gc,reference.get is"+reference.get());
System.out.println("after gc,queue is"+queue.poll());
}
static class BigObject{}
}
bigObject是一个强引用,导致new BigObject()的内存空间不会被GC回收。最终打印结果如下:
before gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
before gc, queue is null
after gc, reference.get is com.danny.lagoumemoryleak.WeakRefDemo$BigObject@7852e922
after gc, queue is null
实现思路
LeakCanary中对内存泄漏检测的核心原理就是基于WeakReference和ReferenceQueue实现的。
- 当一个Activity需要被回收时,就将其包装到一个WeakReference中,并且在WeakReference的构造器中传入自定义的ReferenceQueue;
- 然后给包装后的WeakReference做一个标记key,并且在一个强引用Set中添加相应的Key记录;
- 最后主动触发GC,遍历自定义ReferenceQueue中所有的记录,并根据获取Reference对象将Set中的记录也删除;
- 经过上面3步之后,还保留在Set中的就是,应该被GC回收,但实际还保留在内存中的对象,也就是发生泄漏的对象。
源码分析
我们知道,一个可回收对象在System.gc()之后就应该被GC回收。可是,在Android APP中,我们并不清楚何时系统会回收Activity。但是,按照正常流程,当Activity调用onDestroy方法时就说明这个Activity就已经处于无用状态了。因此我们需要监听每一个Activity的onDestory方法的调用。
ActivityRefWatcher
LeakCanary中监听Activity声明周期是由ActivityRefWatcher来负责的,主要是通过注册Android系统提供的ActivityLifecycleCallbacks,来监听Activity的生命周期方法的调用。当监听到Activity的onDestory方法后,会将其传给RefWatcher的watch方法。
RefWatcher
它是LeakCanary的一个核心类,用来检测一个对象是否发生内存泄漏。主要实现时在watch方法中,如下所示:
public void watch(Object watchReference){
watch(watchReference,"");
}
public void watch(Object watchedReference, String referenceName){
...
String key = UUID.randomUUID().toString();
retainedKeys.add(key); // 1
final KeyWeakReference reference = new KeyedWeakReference(watchReference, key, referenceName, queue); // 2
ensureGoneAsync(watchStartNanoTime , reference); // 3
}
解释说明:
- 第一处生成一个随机的字符串key,这个key就是用来标识WeakReference的,就相当于给WeakReference打了个标签;
- 第二处将被检测对象包装到一个WeakReference中,并将其标识为步骤1中生成的key;
- 第三处调用ensureGoneAsync开始执行检测操作;
因此关键代码就是在ensureGoneAsync方法中,代码如下:
private void ensureGoneAsync(final long watchStartNanoTime, Final KeyedWeakReference reference){
watchExecutor.execute(new Retryable(){
@Override
public Retryable.Result run(){
return ensureGone(reference, watchStartNanoTime);
}
});
}
通过WatchExecutor执行了一个重载方法ensureGone。ensureGone中实现了内存泄漏的检测,方法具体如下:
Retryable.Result ensureGone(final KeyedWeakReference reference ,final long watchStartNanoTime){
removeWeaklyReachableReferences(); // 1
if(!gone(reference)){ // 2
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap();
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() -startDumpHeap);
heapdumpListener.analyze(new HeapDump(heapDumpFile , reference.key,reference.name,excludeRefs,watchDurationMs,gcDurationMs,heapDumpDurationMs)); // 3
return DONE;
}
}
解释说明:
- 1处会遍历ReferenceQueue中所有的元素,并根据每个元素的key,相应的将集合retainedKeys中的元素也删除;
- 2处判断集合retainedKeys是否还包含被检测对象的弱引用,如果包含说明被检测对象并没有被回收,也就是发生了内存泄漏。
- 3处生成Heap堆信息,并生成内存泄漏的分析报告,上报给程序开发人员。
removeWeaklyReachableReferences()方法如下:
private void removeWeaklyReachableReferences(){
KeyWeakReference ref;
while((ref = (KeyWeakReference) queue.poll()) != null){
retainedKeys.remove(ref.key);
}
}
可以看出这个方法的主要目的就是从retainedKeys中移出已经被回收的WeakReference的标志。
gone(reference)方法判断reference是否被回收了,如下:
private boolean gone(KeyedWeakReference reference){
return !retainedKeys.contains(reference.key);
}
实现很简单,只要在retainedKeys中不包含此reference,就说明WeakReference引用的对象已经被回收。
LeakCanary的实现原理比较简单,但是内部实现还有一些其他的细节值得我们注意。
内存泄漏检测时机
很显然,这种内存泄漏的检测与分析是比较消耗性能的,因此为了不影响UI线程的渲染,LeakCanary也做了些优化操作。在ensureGoneAsync方法中调用了WatchExecutor的execute方法来执行检测操作,如下:
private void ensureGoneAsync(final long watchStartNanoTime, final
KeyedWeakReference reference){
watchExecutor.execute(new Retryable(){
@Override
public Retryable.Result run(){
return ensureGone(reference, watchStartNanoTime);
}
});
}
@Override
public void execute(Retryable retryable){
...
waitForIdle(retryable, 0);
...
}
void waitForIdle(final Retryable retryable, final int failedAttempts){
//This needs to be called from the main thread
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHanlder(){
@Override public boolean queueIdle(){
postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}
可以看出,实际上是向主线程MessageQueue中插入了一个IdleHandler,IdleHandler只会在主线程空闲时才会被Looper从队列中取出来并执行。因此能够有效避免内层检测占用UI渲染时间。
通过addIdleHanlder也经常用来做APP的启动优化,比如在Application的onCreate方法中经常做3方库的初始化工作。可以将优先级较低、暂时使用不到的3方库的初始化操作放到IdleHandler中,从而加快Application的启动过程。其实应该叫addIdleMessage更适合些,因为向MessageQueue中插入的都是Message对象。
特殊机型适配
因为有些特殊机型的系统本身就存在一些内存泄漏的情况,导致Activity不被回收,所以在检测内存泄漏时,需要将这些情况排除在外。在LeakCanary的初始化方法install中,通过excludedRefs方法制定了一系列需要忽略的场景。
public static RefWatcher install(Application application){
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
}
这些场景被枚举在AndroidExcludedRefs中,这种统一规避特殊机型的方式,也值得我们借鉴,因为国内的手机厂商实在是太多了。
LeakCanary如何检测其他类
LeakCanary默认只能检测Activity的泄漏,但是RefWatcher的watch方法传入的参数实际是Object,所以理论是是可以检测任何类的。LeakCanary的install方法会返回一个RefWatcher对象,我们只需要在Application中保存此RefWatcher对象,然后将需要被检测的对象传给watch方法即可。
public class MyApp extends Application{
private RefWatcher watcher;
@Override
public void onCreate(){
super.onCreate();
watcher = LeakCanary.install(this);
}
public RefWatcher getWatcher(){
return watcher;
}
}
//MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
((MyApp)getApplication()).getWatcher().watch(testedObj);
}
总结
本节课主要介绍了Android内存泄漏优化的相关知识,主要分为两部分:
- 内存泄漏预防 这需要我们了解JVM发生内存泄漏的原因,并在开发阶段养成良好的编程规范,避免引入会发生内存泄漏的代码。阿里巴巴的代码规范插件,能够起到一定的代码检测效果。
- 内存泄漏检测 内存泄漏检测工具有很多,Android Studio自带的Profiler,以及MAT都是不错的选择。但是相比较而言,使用这些工具排查内存泄漏门槛稍高,并且全部是手动操作,略显麻烦。我们可以使用一个自动检测内存泄漏的开源库--LeakCanary。