Android性能优化--启动优化

1.前言

一个应用App的启动速度能够影响用户的首次体验,启动速度较慢(感官上)的应用可能导致用户再次开启App的意图下降,或者卸载放弃该应用程序。本文会通过以下几个方面来介绍应用启动的相关指标和优化,提供应用的启动速度。

整体文章思路如下:
启动优化.png

2.冷启动&热启动

通常来说,启动方式分为两种:冷启动和热启动。

  1. 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
  2. 热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。

两者之间的特点如下:

  • 冷启动:系统会重新创建一个新的进程分配给该应用,从Application创建到UI绘制等相关流程都会执行一次。
  • 热启动:应用还在后台,因此该启动方式不会重建Application,只会重新绘制UI等相关流程。

冷热启动时间的计算命令:

adb shell am start -W [packageName]/[packageName.XxxActivity]
  • 参数说明:
    1、ThisTime:一般和TotalTime时间一样。除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
    2、TotalTime:应用的启动时间。包含创建进程+Application初始化+Activity初始化到界面显示。
    3、WaitTime:一般比TotalTime大点,包含系统影响的耗时。
    对于我们的应用来说结果如下:
  • 冷启动
    冷启动.png
  • 热启动
    热启动.png

    可以看到两者时间相差比较大。
    根据该命令基本可以看出一个应用的启动速度了,从冷启动热启动的相关关系,当我们需要优化启动速度的时候,优化冷启动速度即可。
    但是该命令我们只是大概知道应用的启动速度,但并不知道我们的应用具体哪个位置耗时,影响启动速度,后续我会介绍如何获取启动具体耗时时间。

3. 常规获取时间方法

常规获取时间方法无非就是在方法执行前记录下时间,在方法执行完毕后记录时间,两者时间之差就是该方法执行的时间,封装一个基础类如下:

public class LaunchTimer {

    private static final String TAG = "LaunchTimer";
    private static long sTime;

    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }

    public static void endRecord() {
        long cost = System.currentTimeMillis() - sTime;
        NLog.i(TAG, "执行耗时:%s", cost);
    }

}

使用方式如下,可以直观的看出createController方法执行的时间

createController耗时.png

这样已经很直观了,可以具体到该方法的执行时间,如果要继续分析则对该方法内部继续执行该代码即可。但是这里有一个问题如果要知道10个或者更多方法的执行时间,这个方法看起来是可以,但写起来过于繁琐,且不符合程序员的习惯,关于这种场景后面会介绍如何处理。

4. TraceView和SysTrace工具使用

  • TraceView使用:TraceView是Android平台配备一个很好的性能分析工具,它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到方法。
    使用方式:
  1. 通过Android studio自带的traceview查看(Android profiler)。
  2. 通过Android SDK自带的Debug。
  3. 通过DDMS中的traceview查看。
    本文主要介绍第二种方式,通过sdk中的方法,对应用进行打点获取相关信息:
 @Override
    public void onCreate(final Bundle icicle) {
        setTheme(R.style.BrowserTheme);
        Intent intent = getIntent();
        NLog.i(LOGTAG,"onCreate");
        super.onCreate(icicle);
        //开始记录,且该方法可以设置文件大小和路径
        Debug.startMethodTracing("browser.trace");
        Controller controller=createController();
        mController = controller;
         getWindow().getDecorView().setSystemUiVisibility(
             View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        controller.handleThirdPartyIntent(intent);
        //结束记录
        Debug.stopMethodTracing();
    }

如上可以在目录下可以生成如下文件

/sdcard/Android/data/com.xxx.xx.browser/files/browser.trace

导出改文件,通过Android Studio的profile打开改文件
traceview.png

1处可以看出有多少线程。
2处可以看出具体方法的耗时。
3处有两个选项:

  • wall clock time:代码在线程上执行的真正时间[有一部分是等待cpu轮询时间]
  • thread time :cpu执行的时间
    一般是优化的是cpu执行时间
    关于该图如何查看和阅读可以参照文章Android性能优化—TraceView的使用
    结合业务代码走查发现Controller0线程为一个线程,因此主线程一些操作可以放进去执行,从而减少main线程的耗时。走查代码可以发现在Controller0线程中执行如下
        requestPermission();
        ExecutorService service = Executors.newSingleThreadExecutor(new NamedThreadFactory("Controller"));
        Future<Boolean> future = service.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                try {
                    asyncInit();

                } catch (Exception e) {
                    return false;
                }
                return true;
            }
        });

requestPermission()方法执行在main线程中,因此我们可以把其放在Controller0线程中执行,从而减少main线程的的时间

  ExecutorService service = Executors.newSingleThreadExecutor(new NamedThreadFactory("Controller"));
        Future<Boolean> future = service.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                try {
                    requestPermission();
                    asyncInit();

                } catch (Exception e) {
                    return false;
                }
                return true;
            }
        });

经测试发现无问题,且对比此时的trace文件发现修改前后main线程时间相对来说减少很多。

  • SysTrace使用:
    SysTrace用于收集可帮助您检查原生系统进程的详细系统级数据,例如CPU调度、磁盘活动、应用线程等,并解决掉帧引起的界面卡顿。
    使用方式:

在代码的开始位置加上tag

  TraceCompat.beginSection("AppOnCreate");

然后指定位置结束

TraceCompat.endSection();

,即可以抓取到整个应用在此过程的相关信息,例如在onCreate方法中添加上述两行代码,执行相关python命令:

python systrace.py -b 32768 -t 10 -a com.xxx.xxx.browser -o browser.html sched gfx view wm am app

操作相关应用,即可以抓取整个过程的相关信息:


systrace.png

即可以看到添加的tag“AppOnCreate”,对应的时间信息:

  1. Wall Duration 代表的方法从开始到结束的耗时
  2. CPU Duration 代表CPU的执行时间
    通过这两个参数可以看出此流程中执行的时间等相关信息
    我们都知道CPU是轮询模式,因此优化的方向可以说是两个方向,提高CPU的核数和优化CPU执行的时间。
    关于Systrace的具体使用及命令可以参照
    性能优化工具(二)-Systrace
    了解 Systrace

5. 通过AOP获取时间

  • AOP:面向切面编程(Aspect-Oriented Programming)。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。打个比方Android 里面PMS,AMS都拥有各自的职责,但是他们都需要通过log系统管理log,这就是一种AOP思想。

AspectJ实际上是对AOP编程思想的一个实践,当然,除了AspectJ以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。
AspectJ的使用如下:
根目录gradle下引用:

 classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'

app目录gradle文件下引用:

 implementation 'org.aspectj:aspectjrt:1.8.+'

此两处引用完成之后,就是代码编写:

package com.xx.xxx.browser.aspect;


import android.util.Log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class IntercepLifeCycleAOP {
    //获取该Activity下的所有on开头的方法耗时
    @Around("execution(* com.xxx.xxx.BrowserActivity.on**(..))")
    public Object getTime(ProceedingJoinPoint joinPoint) {
        Object proceed = null;
        long start = System.currentTimeMillis();
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        long end = System.currentTimeMillis();
        Log.d("IntercepLifeCycleAOP", joinPoint.getSignature().getName() + ":执行时间:" + (end - start));
        return proceed;

    }

}

引入之后结果如下:

AspectJ.png

可以看到具体方法的耗时。
采用注解方式,其中Around 需要有一定的AspectJ相关的语法,具体参考:
AspectJ 在 Android 中的使用
gradle_plugin_android_aspectjx
hugo

6 . 用户体验优化

  1. 主题优化:通过给应用设置一个透明主题,在应用启动完成之后,再给其赋予本该有的主题,通过对启动页的主题设置后,就会将白屏/黑屏抹去,用户点击App的图标就展示启动图,让用户先产生启动很快的“错觉”。
  2. 动画兼容:根据不同年代的机型可以选择执行或不执行相关动画,或者延迟其他相关操作,可根据device-year-class来判断具体年份
    3.UI布局优化。

7. 异步加载

  1. 采用线程加载一些资源,比如sdk初始化,配置信息拉取等相关资源。线程,线程池,IntentServices均可以,配合延迟效果更好。
  2. 当我们采用线程之间的可能会存在各线程之间相互等待依赖等相关问题,资源A线程必须在资源B加载完成,才能加载,但两者又会在不同的线程之间,此时简单的办法可以采用CountDownLatch来实现。其整体思路如下图
    CountDownLatch.png
    ,具体使用方法可以参照文章Android 并发之CountDownLatch、CyclicBarrier的简单应用(三)
  3. 使用 Pipeline 机制,根据业务优先级规定业务初始化时机,制定启动框架,它们为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。无论是微信的mmkernel 还是阿里的Alpha 都具备这种能力。
  4. 其他方案:
    除了上述几种,我们也可以利用IdealHandler,dex分包等相关方式做到启动优化。

8 总结

上面主要介绍了如何获取启动的相关事件和相关优化知识点。关于时间就是尽量使用工具,关于优化整体思路就是能预加载能延迟加载的资源尽量去预加载去延迟加载,能异步的业务尽量异步。
当然优化这个话题也是要根据具体的业务逻辑来定,总之:

对于启动优化要警惕 KPI 化,我们要解决的不是一个数字,而是用户真正的体验问题。

上述只是提供一些思路和方式,还有很多奇淫技巧,欢迎给位大佬评论指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343