面试官:说一说Android启动优化

应用的启动速度对一个APP来说至关重要,会直接影响到用户体验,如果启动速度过慢会导致用户的流失,本文就启动速度优化分析,为优化启动速度提供一些思路。

1、应用启动分类

应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动和热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。

1.1、冷启动

app冷启动可以分为两个阶段

第一阶段

1、加载并启动app

2、启动后立即显示一个空白的启动窗口

3、创建app进程

第二阶段

1、创建app对象

2、启动主线程

3、创建主Activity

4、加载布局

5、布置屏幕

6、首帧绘制

一旦应用进程完成首帧绘制,系统进程就会换掉当前显示的后台窗口,替换为主Activity。此时,用户可以开始使用应用

冷启动中,作为开发者,能干预的部分主要是Application的OnCreate阶段和Activity的onCreate阶段,如下图:

1.2、热启动

热启动时,系统将activity放到前台。如果应用程序的所有activity存在内存中,则应用程序可以避免重复对象初始化、渲染、绘制操作

1.3、温启动

温启动时,由于app的进程仍然存在,执行的是冷启动的第二阶段

1、创建app对象

2、启动主线程

3、创建主Activity

4、加载布局

5、布置屏幕

6、首帧绘制

温启动常见场景:

1、用户退出应用后又重启应用。进程可能继续运行。但应用从调用Activity的onCreate方法开始重新执行

2、内存不足,系统将应用杀死,然后用户重新启动。进程和Activity需要重启,但传递到onCreate的已保存的实例bundle对于完成启动有一定帮助

接下来来看下如何获得启动时间~

2、获取启动时间

2.1、使用adb命令获取

adb shell am start -W [packageName]/[packageName.xxActivity]

adb shell am start -W com.tg.test.gp/com.test.demo.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.tg.test.gp/com.test.demo.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.tg.test.gp/com.test.demo.MainActivity
ThisTime: 1344
TotalTime: 1344
WaitTime: 1346
Complete

ThisTime: 最后一个Activity启动耗时

TotalTime: 启动时经历的所有Activity总耗时

WaitTime: AMS启动所有Activity的总时间(包括启动目标应用前的)

2.2、使用代码打点

public class LaunchTimer {

    private static long sTime;

    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }
    public static void endRecord() {
        endRecord("");
    }
    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - sTime;
        LogUtils.i(msg + "cost " + cost);
    }
}

该方式一般为Application初始化attachBaseContext方法打启动开始时间戳,应用用户可操作界面完全展示可操作后打结束时间戳,两时间差即为启动耗时

但这种方式并不优雅,如果启动逻辑复杂,方法很多的情况下,最好采用aop进行优化。

3、应用启动优化分析工具

3.1、TraceView

Traceview是Android自带的性能分析工具,可图形化展示方法调用时间,调用栈,还可以查看所有线程信息,对分析方法耗时,调用链是非常好的工具

使用方法是采用代码埋点的方式:

1、在开始收集的位置,执行Debug.startMethodTracing("app_trace"),其中参数为自定义文件名

2、在结束收集的位置,执行Debug.endMethodTracing()

文件生成路径为/sdcard/Android/data/包名/files/文件名.trace,使用Android Studio可以打开该文件

举个例子:

我们来看下testAdd这个方法的耗时

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAdd(3, 5);
    }

    private void testAdd(int a, int b) {
        Debug.startMethodTracing("app_trace");
        int c = a + b;
        Log.i("Restart", "c =  a + b = " + c);
        Debug.stopMethodTracing();
    }
}

运行程序后,找到/sdcard/Android/data/com.restart.perference/files/app_trace.trace文件,在AndroidStudio中双击它,是可以解析出文件中的信息的,打开后如下:

下面来看下,CallChart, FlameChart, Top Down, Bottom Up分别是怎么用的。首先要选择好时间区域,像本案例中,因为程序很简单,能分析的区域比较小,选中它后,可得到下图:

Call Chart得到的图形中,可以看到程序整个调用栈,同时可以看到方法耗时。

比如图中的testAdd方法,先后调用了Debug.startMethodTracing、Log.i、Debug.stopMethodTracing方法。同时从图中可以看出startMethodTracing方法比stopMethodTracing方法耗时长。在实际优化中,找到哪个方法耗时长,针对性优化是非常有作用的。

分析时,第三方程序,系统代码我们一般是优化不了的,CallChart非常贴心地用颜色帮我们区分哪些是我们自己写的代码。橙色的是系统API,蓝色一般是第三方API,绿色的才是我们自己写的。比如图中的testAdd方法,我们自己写的是可以通过调整优化的。

Flame Chart是Call Chart的倒序图,作用相似,图形如下:

Top Down可以看到每个方法内部调用了哪些方法,以及每个方法的耗时,耗时占比,相比于Call Chart的图形查找,Top Down则是更具体,有具体的方法耗时数据

图中的Total代表方法总共的耗时,self代表方法内非方法调用的代码耗时,Children代表方法内调用的其他方法的耗时。

同样以testAdd方法为例,testAdd方法总耗时为3840us,占程序运行时间97%,testAdd方法中自身代码耗时为342us,占比为8.65%,testAdd方法中调用的其他方法总共耗时3498us,占比88.52%

private void testAdd(int a, int b) {
        Debug.startMethodTracing("app_trace");//算到Children中
        int c = a + b;//这一句是算在self耗时中,耗时其实很短
        Log.i("Restart", "c =  a + b = " + c);//算到Children中
        Debug.stopMethodTracing();//算到Children中
}

Bottom Up是Top Down的倒序图,可以看方法是被哪个方法调用的

TraceView中还有个选项值得注意,

在右上角有个Wall Clock TimeThread Time的选项,其中Wall Clock Time的意思是方法执行的实际耗时,而Thread Time指的是CPU耗时。我们平时说的优化更多的是优化CPU时间,当有IO操作时,用Thread Time来分析耗时更合理

此外,使用TraceView需要关注TraceView的运行时开销,因为它自身耗时较长,就有可能会带偏我们的优化方向。

3.2、Systrace

Systrace结合Android内核的数据,生成HTML报告

systrace在Android/sdk/platform-tools/systrace目录中。使用前需要安装python,因为systrace是用python生成html

报告的,命令如下:

python systrace.py -b 32768 -t 10 -a 包名 -o perference.html sched gfx view wm am app

具体参数可参考:developer.android.google.cn/studio/prof…

执行命令后,打开报告,显示如下

要使用chrome浏览器打开,否则可能会显示白屏。如果使用chrome也显示白屏,可在chrome浏览器中输入chrome:tracing, 再Load文件进去显示

查看图时,A键是左移,D键右移, S键缩小,W键放大

4、常用优化

4.1、启动加载常见优化策略

一个应用越大,涉及模块越多,包含的服务甚至进程就会越多,如网络模块的初始化,底层数据初始化等,这些加载都需要提前准备好,有些不必要的就不要放到应用中。通常可以从以下四个维度整理启动的各个点:

1、必要且耗时:启动初始化,考虑用线程来初始化

2、必要不耗时:不用处理

3、非必要耗时,数据上报、插件初始化,按需处理

4、非必要不耗时:直接去掉,有需要的时候再加载

将应用启动时要执行的内容按上述分类,按需实现加载逻辑。那么常见的优化加载策略有哪些呢?

异步加载:耗时多的加载放到子线程中异步执行

延迟加载: 非必须的数据延迟加载

提前加载:利用ContentProvider提前进行初始化

下面分别介绍异步加载和延迟加载的一些常用处理

4.2、异步加载

异步加载,简单来说,就是使用子线程异步加载。在实际场景中,启动时常常需要对各种第三方库做初始化操作。通过将初始化放到子线程中进行,可以大大加快启动。

但是通常,有些业务逻辑是要再第三方库的初始化后才能正常运行的,这时候如果只是简单的放到子线程中跑,不做限制就很可能出现在没初始化完成就跑业务逻辑,导致异常。

这种较为复杂的情况下,可以采用CountDownLatch处理,或者是使用启动器的思想处理。

CountDownLatch使用

class MyApplication extends Application {

    // 线程等待锁
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);

    // CPU核数
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // 核心线程数
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

    void onCreate() {
        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override public void run() {
                //初始化weex,因为Activity加载布局要用到需要提前初始化完成
                initWeex();
                mCountDownLatch.countDown();
            }
        });

        service.submit(new Runnable() {
            @Override public void run() {
                //初始化Bugly,无需关心是否在界面绘制前初始化完
                initBugly();
            }
        });

        //提交其他库初始化,此处省略。。。

        try {
            //等待weex初始化完才走完onCreate
            mCountDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用CountDownLatch在初始化的逻辑不复杂的情况下推荐使用。但如果初始化的几个库之间又有相互依赖,逻辑复杂的情况下,则推荐使用加载器的方式。

启动器

启动器的核心如下:

  • 充分利用CPU多核能力,自动梳理并顺序执行任务;
  • 代码Task化,将启动任务抽象成各个task;
  • 根据所有任务依赖关系排序生成一个有向无环图;
  • 多线程按照线程优先级顺序执行

具体实现可参考:github.com/NoEndToLF/A…

4.3、延迟加载

有些第三方库的初始化其实优先级并不高,可以按需加载。或者是利用IdleHandler在主线程空闲的时候进行分批初始化。

按需加载可根据具体情况实现,这里不做赘述。这里介绍下使用IdleHandler的使用

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //当return true时,会移除掉该IdleHandler,不再回调,当为false,则下次主线程空闲时会再次回调
            return false;
        }
    };

使用IdleHandler做分批初始化,为什么要分批?当主线程空闲时,执行IdleHandler,但如果IdleHandler内容太多,则还是会导致卡顿。因此最好是将初始化操作分批在主线程空闲时进行

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //每次执行一个Task,实现分批进行
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            //当为空时,返回false,移除IdleHandler
            return !mDelayTasks.isEmpty();
        }
    };

    //添加初始化任务
    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    //给主线程添加IdleHandler
    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

4.4、提前加载

上述方案中初始化最快的时机都是在Application的onCreate中进行,但还有更早的方式。ContentProvider的onCreate是在Application的attachBaseContext和onCreate方法中间进行的。也就是说它比Application的onCreate方法更早执行。所以可以利用这点来对第三方库的初始化进行提前加载。

androidx-startup使用

如何使用:
第一步,写一个类实现Initializer,泛型为返回的实例,如果不需要的话,就写Unit
class TimberInitializer : Initializer<Unit> {

    //这里写初始化执行的内容,并返回初始化实例
    override fun create(context: Context) {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
            Timber.d("TimberInitializer is initialized.")
        }
    }

    //这里写初始化的东西依赖的另外的初始化器,没有的时候返回空List
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }

}

第二步,在AndroidManifest中声明provider,并配置meta-data写初始化的类
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="com.test.pokedex.androidx-startup"
    android:exported=“false"
    //这里写merge是因为其他模块可能也有同样的provider声明,做合并操作
    tools:node="merge">
    //当有相互依赖的情况下,写顶层的初始化器就可以,其依赖的会自动搜索到
    <meta-data
        android:name="com.test.pokedex.initializer.TimberInitializer"
        android:value="androidx.startup" />
</provider>

4.5、其他优化

1、在应用中,增加启动默认图或者自定义一个Theme,在Activity首先使用一个默认的界面解决部分启动短暂黑白屏问题。如android:theme="@style/Theme.AppStartLoad"

5、小结

1、冷启动、温启动、热启动主要进行的处理及他们的区别

2、如何获取启动时间,介绍了使用adb命令和代码打点这两种方式

3、如何使用工具找到程序中耗时较长的代码,介绍TraceView和Systrace的使用

4、常见的启动优化方式及实现介绍(异步加载,延迟加载,提前加载等),关键思想:异步、延迟、懒加载

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

推荐阅读更多精彩内容