Android应用启动速度优化全解

1.应用启动的类型

冷启动

冷启动指的是该应用程序在此之前没有被创建,发生在应用程序首次启动或者自上次被终止后的再次启动。简单的说就是app进程还没有,需要创建app的进程启动app。
比如开机后,点击屏幕的app图标启动应用。

冷启动的过程主要分为两步:
1)系统任务。加载并启动应用程序;显示应用程序的空白启动窗口;创建APP进程
2)APP进程任务。启动主线程;创建Activity;加载布局;屏幕布局;绘制屏幕
其实这不就是APP的启动流程嘛?所以冷启动是会完整走完一个启动流程的,从系统到进程。

温启动

温启动指的是App进程存在,但Activity可能因为内存不足被回收,这时候启动App不需要重新创建进程,只需要执行APP进程中的一些任务,比如创建Activity。

比如返回主页后,又继续使用其他的APP,时间久了或者打开的应用多了,之前应用的Activity有可能被回收了,但是进程还在。
所以温启动相当于执行了冷启动的第二过程,也就是APP进程任务,需要重新启动线程,Activity等。

热启动

热启动就是App进程存在,并且Activity对象仍然存在内存中没有被回收。
比如app被切到后台,再次启动app的过程。
所以热启动的开销最少,这个过程只会把Activity从后台展示到前台,无需初始化,布局绘制等工作。
那我们所要做的启动速度优化呢,就是对于冷启动这种情况进行的。

2.启动耗时统计:

2.1adb命令查看耗时:

如果是本地调试的话,统计启动时间还是很简单的,通过命令行方式即可:

adb shell am start -W packagename/activity

例如:

adb shell am start -W com.xxx.xxxx/com.xxxx.login.activity.LauncherActivity

得到冷启动时间:


WaitTime
返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
ThisTime
表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
TotalTime
表示新应用启动的耗时,包括新进程的启动和 Activity 的启动;

一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。

多长时间内app启动完成才算秒开?

1秒
既然谈论的目标是秒开率,那必然是需要 1s 内完成用户的启动流程。

启动流程终点选取

大多数的 App 在选择冷启动启动流程终点时,会选择首页 Activity 的 onWindowFocusChanged 时机,此时首页 Activity 已经可见但其内部的 View 还不可见,对于用户侧已经可以看见首页背景,同时会将首页内 View 绘制归入首刷过程中。

2.2 代码打点(函数插桩)

可以写一个统计耗时的工具类来记录整个过程的耗时情况。其中需要注意的有:
在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
在项目中核心基类的关键回调函数和核心方法中加入打点。

class LaunchRecord {

    companion object {

        private var sStart: Long = 0
        
        fun startRecord() {
            sStart = System.currentTimeMillis()
        }

        fun endRecord() {
            endRecord("")
        }

        fun endRecord(postion: String) {
            val cost = System.currentTimeMillis() - sStart
            println("===$postion===$cost")
        }
    }
}
class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        //开始打点
        LaunchRecord.startRecord()
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mTextView.viewTreeObserver.addOnDrawListener {
            LaunchRecord.endRecord("onDraw")
        }

    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LaunchRecord.endRecord("onWindowFocusChanged")
    }
}
2.3获取方法耗时

在进程启动过程的调用方法前后加入计时器,打印调用方法耗时数据。

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        initFresco();
        initBugly();
        initWeex();
    }

    private void initWeex(){
        LaunchRecord.Companion.startRecord();
        InitConfig config = new InitConfig.Builder().build();
        WXSDKEngine.initialize(this, config);
        LaunchRecord.Companion.endRecord("initWeex");
    }

    private void initFresco() {
        LaunchRecord.Companion.startRecord();
        Fresco.initialize(this);
        LaunchRecord.Companion.endRecord("initFresco");
    }

    private void initBugly() {
        LaunchRecord.Companion.startRecord();
        CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
        LaunchRecord.Companion.endRecord("initBugly");
    }
}
2.4 AOP面向切面编程监控耗时

这里是自己写的 ASM+Transform+Gradle plugin完成函数耗时统计插件:https://www.jianshu.com/p/0672899a5189
可以自己平时在开发、测试过程中监测是否发生耗时过多、卡顿的情况发生。

2.5 使用Profile监控应用启动耗时
  • 打开edit configuration


  • 勾选profilling下面 start this recording on startup选项,标识在启动过程中做记录


点击profile app 启动按钮,然后等首页启动完成,点击stop,得到启动这个过程的启动数据。启动过程的观察类型,就选用Top down,按照时间先后从上到下的调用栈顺序来查看方法执行耗时。

这里是页面启动调用栈:


这里可以看到,启动页的activity调用的各个函数耗时,这里onCreate方法执行初始化花费377ms,setContentView花费23ms。为什么onCreate里面花费了377ms呢,因为我在onCreate中调用了costTime()函数,其他代码初始化实际花费是77ms。

costTime函数:

    fun costTime() {
        Thread.sleep(300)
    }

至此,启动速度优化要解决的问题找到了,就是这个costTime函数影响了启动速度,需要对这块代码做异步初始化或者延迟执行。

3.启动优化操作:

3.1. 添加一个默认的启动页的Theme能从现象解决这个问题,但是并没有从根本上解决启动慢的问题。
3.2异步加载初始化

Application中主要做了各种三方组件的初始化,参数设置,启动各种服务等;
对于过多的初始化任务,我们考虑以下优化方案:

1.考虑异步初始化三方组件,不阻塞主线程;
2.延迟部分三方组件的初始化;

使用启动器来异步加载初始化任务:


启动器流程:
(1)代码Task化,启动逻辑抽象为Task
(2)根据所有任务依赖关系排序生成一个有向无环图
(3)多线程按照排序后的优先级依次执行

比如创建一个ARouterTask:

class ARouterTask(val application: BaseApplication) : Task("") {

    override fun run() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(application)
    }

}

将task添加进project的task列表中:

    fun  createCommonTaskGroup() : Task {
        var arouterTask = ARouterTask(this)
        var refreshTask = RefreshTask()
        var builder = Project.Builder()
        builder.add(arouterTask)
        builder.add(refreshTask)
        return builder.create()
    }

或者用after给task带有顺序:

 Project.Builder builder = new Project.Builder();
        builder.add(a);
        builder.add(b).after(a);
        builder.add(c).after(a);
        builder.add(d).after(b, c);
        builder.add(e).after(a);
        Project group = builder.create();

最后,manager添加project并start各个异步任务。

        AlphaManager.getInstance(applicationContext).addProject(createCommonTaskGroup())
        AlphaManager.getInstance(applicationContext).start()
3.3延迟初始化

IdleHandler使用:https://www.jianshu.com/p/ab3510efe98f

3.4 懒加载

懒加载就是有些Task只有在特定的页面才会使用,这时候我们就没必要将这些Task放在Application中初始化了,我们可以将其放在进入页面后在进行初始化。

参考:
https://juejin.cn/post/7306692609497546752
https://heapdump.cn/article/3624814
https://blog.csdn.net/u011578734/article/details/109496667
https://segmentfault.com/a/1190000020904556
https://juejin.cn/post/6844904093786308622#heading-132
阿里异步启动框架
https://mp.weixin.qq.com/s/dwhBnvMe-ePa2HSreEWsUw

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

推荐阅读更多精彩内容