Android进阶课学习(24-26)

17 篇文章 1 订阅
10 篇文章 1 订阅

第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方法中需要做以下几件事情:

  1. 收集crash现场的相关信息,如当前APP版本信息、以及手机设备的相关信息等;
  2. 日志的记录工作,将收集到的信息及系统抛出的异常信息保存到本地,比如以文件的方式保存。
  3. 使用的时候,在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内存泄漏的场景主要有以下几种情况:

  1. 将Context或者View置为static;   
          View默认会持有一个Context的引用,如果将其置为static将会造成View在方法区中无法被快速回收,最终导致Activity内存泄漏。
    public class ActivityA extend AppCompatActivity{
        private static ImageView imageView;
        ...
    }

    imageView会导致ActivityA无法被GC回收。

  2. 未解注册各种Listener,比如广播;
  3. 非静态Handler导致Activity泄漏;
  4. 三方库使用Context或单例使用的Context不当;
        在项目中使用各种三方库,有些初始化的时候需要传入一个Context对象,但其内部有可能一直持有此Context引用,比如将其设置为static,这样如果外部传入的Context是Activity,就会导致外部Activity的隐形泄漏,一般在SDK开发中,尽量使用外部传入的Context的getApplicationContext方法获取全局上下文,而不要直接使用外部传入的Context。

内存泄漏检测

LeakCanary是Square公司的一个开源库,通过它可以在APP运行过程中检测内存泄漏,当内存泄漏发生时,会生成泄漏对象的引用链,并通知程序开发人员。

LeakCanary主要分2大核心部分:

  1. 如何检测内存泄漏;
  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实现的。

  1. 当一个Activity需要被回收时,就将其包装到一个WeakReference中,并且在WeakReference的构造器中传入自定义的ReferenceQueue;
  2. 然后给包装后的WeakReference做一个标记key,并且在一个强引用Set中添加相应的Key记录;
  3. 最后主动触发GC,遍历自定义ReferenceQueue中所有的记录,并根据获取Reference对象将Set中的记录也删除;
  4. 经过上面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
}

解释说明:

  1. 第一处生成一个随机的字符串key,这个key就是用来标识WeakReference的,就相当于给WeakReference打了个标签;
  2. 第二处将被检测对象包装到一个WeakReference中,并将其标识为步骤1中生成的key;
  3. 第三处调用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. 1处会遍历ReferenceQueue中所有的元素,并根据每个元素的key,相应的将集合retainedKeys中的元素也删除;
  2. 2处判断集合retainedKeys是否还包含被检测对象的弱引用,如果包含说明被检测对象并没有被回收,也就是发生了内存泄漏。
  3. 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。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值