写点什么

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

用户头像
Android架构
关注
发布于: 11 小时前

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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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 中双击它,是可以解析出文件中的信息的,打开后如下:


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2s1gpZz-1616421092820)(https://upload-images.jianshu.io/upload_images/25094154-7d9a059e870d906c.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]


下面来看下,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 Time Thread 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架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
面试官:说一说Android启动优化