一、启动优化概念
1.1、为什么要做启动优化?
APP优化是我们进阶高级开发工程师的必经之路,而APP启动速度的优化,也是我们开启APP优化的第一步。用户在使用我们的软件时,交互Shell最多最频繁的也就是APP的启动页面,如果启动页面加载过慢,很可能造成用户对我们APP的印象过差,进而消耗了用户的耐心,更严重可能导致用户的卸载行为。这也是微信始终坚持使用“一个小人望着地球”函数调用可以出现在表达式中吗作为启动页面的背景,并且坚持不添加启动广告的的原因。
1.2、启动分类
冷启动线程池面试题: 特点是shell翻译耗时最多,同时它也是衡量标准,我函数调用语句们在线上做的各种优化都是以它作为标准,从下面函数调用是什么意思这张图片可以看出冷启动它经历了一系列的流程,所以它的初始化sdk什么意思耗时也是最多的。
热启动: 特点是最快,我们所说的热启动是指app从后台切换到前台,它没有applicati初始化on的创建和各种生命周期的调用,所以说这种启动方式是最快的。
温启动: 特点是较工龄差一年工资差多少快,它的速度介于冷启动和热启动之间,对于这种方式它会重走activity的生命周期,不会重走进程的创建,application的创建和生命周期等流程。
1枸杞.3、相关任务
冷启动之前:
- 启动App;
- 加载空白Window;
- 创建进程。
这三个任务都是系统行为,无法进行真正的干预。网上大多介绍启动优化的都是针对第2条,但其实这是一个假的干预,只是对我们肉眼感知上的一个优化。
之后进行宫颈癌的是:
- 创建Application;
- 启动主线程;
- 创建MainActivity;
- 加载布局;
- 布置屏幕;
- 首帧绘制。
我们的优化方向: Application和Activity生命周期的这个阶段,这是开发者真正可以控制的时间。
二、启动时间测量方式
这里介绍两种启动时间的测量方式:
- adb命令
- 手动打点
2.1、adb命令
这种方式是我们通过在终端输入一条adb命令,然后它会shell脚本编程100例打开我们要测试的app,同时进行结果的输出。具体的命令如下:
adb shell am sta工商银行rt -W packagename/首屏Activity(这里需要使用全类名)
这里我以自己写的一个简单的列表展示的Demo工程举例说明:
ThisTime:最后一个线程池有哪几种Activit函数调用的方法y启动耗时
TotalTime:所有Activity启动耗时(这里ThisTime和TotalTim工龄差一年工资差多少e值是一致的,因为我的Demo中只有一个Main线程池创建Activity)
W函数调用可以作为独立的语句存在aitTime:AMS启动Activ线程池的工作原理ity的总耗时,对于一个通用的app函数调用可以作为独立的语句存在(包含SplashActivity)shell什么意思,ThisTime肯定是线程池面试题小于TotalTime的,即:
Thi函数调用语句sTime < TotalTime < WaitTime
总结:这种方式线下使用方便,可以使用这种方式测量竞品为竞品分析提供需要的数据,不能带到线上,并且测量出来的时间也是一个非严谨精确的时间。
2.2、手动打点
这种方式是在app启动开始时埋点,启动结束时埋点,然后计算二者差值。
实际使用中,一般将开始时间这个点埋在Application的attachBaseContex初始化t(Context base)这个方法中,这是整个应用所能接收到的最早的回调时机。开始时间有了,那么结束时间该怎么计算呢,也就是我们应该把结束时间这个点埋在什么位置呢?网上很多初始化电脑时出现问题资料里都会说是在onWindowFocusChangeshell编程d()这个方法里做启动结束的时间计算,但是实际上写在这里其实是有问题的。
误区:onWindowFocusChanged它只是Activit的首帧时间,是activity首次绘制的时间,并不能代表线程池核心参数activity已经展现出来。我们做性能函数调用栈优化的目的是为了改善用户的体验,并不是单纯的为了把启动时间缩短,因为这样做是不准确的,我们需要的是用户真正看到界面的时间,所以正确的情况应该是在真实的数据展示(一般取第一条)出来,才算结束的时间节点。
下面我们就来实战一下该如何在代码中初始化电脑的后果埋点统计启动时间? 首先我们定义一个工具类LaunchTime,用来计算差值时间:
package com.jarchie.performance.utils;
import android.util.Log;
/**
* 描述: 打点计算启动时间
*/
public class LaunchTime {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord(String msg) {
long cost = System.currentTimeMillis() - sTime;
Log.i(msg, "--->cost" + cost);
}
}
然后在Ashellfishpplication中埋下开始时间点:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LaunchTime.startRecord();
}
然后在我们列表适配器中的onB工龄差一年工资差多少indViewHolder中绑公司让员工下班发手机电量截图定数据时统计第一条Item展示出来的时间点:
if (position ==0 && !mHasRecorded){
mHasRecorded = true;
holder.mAllLayout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
holder.mAllLayout.getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTime.endRecord("FirstShow");
return true;
}
});
}
最后我们在MainActivity中的onWindowFocusChanged()方法中统计一下Activity的首帧时间:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
LaunchTime.endRecord("onWindowFocusChanged");
}
现在来运行我们的程序,看一下最终统计出来的时间值究竟是多少?
由上面的结果可以看出首shell是什么意思中文帧时间是904毫秒,线程池的七个参数列表数据的第一条展示的时间是1573毫秒,两者之间的时间差值是超过200毫秒的,这也就表明如果我们仅以Activity的首帧时间作为启动结束,那么这个时shell脚本间明显是偏早的,不符合我们做启动优化的初衷。
三、启动优化工具
以下所介Shell绍的两种方式是互相补充的,我们需要正确认识工具并且能够在不同的场景下选择适合的工具。
3.1、traceview
特点:
- 图形的形式展示执行时间、调用栈等
- 信函数调用的四个步骤息全面,包含所有线程
使用方式:在代码中需要做性能分析的地方开始位置和结束位置插入以下代码
- Debug.startM函数调用的三种方式ethodTracing(“”); //该方法具有重载方法,可设置收取信息的路径,大小
- Debug.stopMethodTracing(“”);
- 生成文件在sd卡:And函数调用可以作为一个函数的形参roid/data/packag初始化电脑的后果ename/files
代码实战:
将我们的项目运行之后(shellfish注意开启运行时权限,这部分不是本节重点,我直接到应用里将权限开启了)生成文件如下:
将生成初始化游戏启动器失败的文件打开,如下图所示,可以看到Threads中是这个应用所有的线程数,我们可以看到线程总数以及对应的每个线程在具体的时间都做了哪些操作,然后下面有四个Tab可以切换线程池,首先来看Call Chart,可以看到在具体的每一行都指向了具体的函数调用,将鼠标移到对应的每一行上面都有具体的执行时间等信息,沿着垂直方向看是具体的初始化电脑时出现问题调用者,比如a调用b,则a在上方b在下方,而且不同的api它的颜色也是不一样的,对于系统api是橙色的,对于应用自身的函数调用颜色是绿色的,对于第三方api调用颜色函数调用的方法是蓝色的(包括java语言的api)。
接着来看Fla工资超过5000怎么扣税me Chart(又叫火焰图),它是一个倒置的调用图表,一般来说它的作用没有第一个大,它shell什么意思会收集相同的调用方顺序完全相同的函数,比如a调b调c并且调函数调用栈用了多次,它会将它们收集公司让员工下班发手机电量截图在一起:
下面这张图是Top Down,它比较直观的展示了函数的调用列表,比如下图中首先main初始化电脑的后果()函数调用了init(),init()又调用了g()等等,相当于Call Chart详细版,并且你将鼠标放在对应函数上右键有个Jump to Source可以跳转到具体的代码中。
Total Time是某个函数执行的总时间,Self Time是该函数体内部自有代码执行的时间,Childre Time是该函数内部调用别的函数所需的时间,后面二者的时间总和一定是等于前面的Total Time的,这点需要注意。Self Time上方有一栏下拉菜单,即我们第一张图中红色标注的菜单栏WallClockTime和ThreadT工龄越长退休金越多吗ime,线程池创建前者是这段代码执行所消耗的时间,后者是CPU执行的时间,一般情况下是前者大于后者,因为一般情况下某个函数消耗的时间并不等于CP工商银行U真正消耗的时间。
最后是Bottom Up,这个的作用也是比较小了,它和Top Down是相反的,它会告诉你某个函数具体是谁调用了它:
总结:一般比较关注的是Call Chart和Top Down
- 运行时开销严重,整体都会变慢(它会抓取当前运函数调用栈行的所有线程的所有执行函数和顺序)
- 由于它非常严重的运行时开销,所以它很有可能回带偏优化方向
3.2、systrace(python脚本)
特线程池点:
- 结合Android内核的数据,生成Html报告
- API18以上使用,推荐TraceCo函数调用的方法mpat
使用方式:
- 一段python函数调用的方法脚本:python systrace.py -tshell翻译 10 [other-options][categories]
- 详见官方文档:developer.androi线程池核心参数d.google.cn函数调用的方法/topic/perfo…
我这里放一张我自己运行的示例,仅供参考:
代码实战Go:
首先将项目运行让我们写的代码生效,然后运行我们的python脚本,启动trgoogleacing之后,点击我们的app函数调用的三种方式让它开始收集信息,tracing完成之后到对应的目录就会发现已经生成了我们的Performance.html文件,我们到浏览器中打开,如下图所示:
由上图中左侧可以看到有CPU的核心数,往下滑动还可以看到各个线程名线程池原理称,然后还可以根据代码中打的Tag来搜索,下方会展示比较详细的trace信息(上图中也举例说明了,需要注意的Wall Time和CPU Time红色部分圈出),点到右侧图中具体的位置都会展示出比较详细的方法名称执行时间等信息。
总结:
- 轻量级,开销小(它是公积金你在哪里埋点,它就处理哪里,这点和traceview不同,需要注意)
- 直观反映cp工龄差一年工资差多少u利用率
- 需要注意cputime与wallt初始化电脑时出现问题ime区别: walltime是代码执行时间,cputime是代码消耗cpu的时间(优化的重点指标)。举个栗子:锁冲突(比如现在调用了A方法,进入A方法之函数调用可以作为一个函数的形参后需要一把锁,但此时这把锁被B所持有,导致代码在shell什么意思A这里停下了,实际上可能线程池创建这个A函数并不耗时,但是由于一直拿不到锁,所以一直处于等待状态,这就导致walltime时间很长,但是它实际上对CPU并没有多少消耗)
关于上述工具的详细使用方法大家可以自行百度或者谷歌查找相关资料,认真学习一下这些分析工具龚俊的使用。
四、优雅获取方法耗时
4.1、常规线程池的使用方函数调用可以作为一个函数的形参式
我们在做启动优化的时候通常需要知道启动阶段所有方法的耗时,这样可以有针对性的分析出耗时较多的方法。一般的实现方式就是通过手动埋点来实现,比如在某个方法开始和结束函数调用的方法的位置分别插入以下代码:
long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;
//或者可以使用这行代码:SystemClock.currentThreadTimeMillis();
//CPU真正执行的时间
当有多个方法需要埋点初始化磁盘时,同理这样写就可以获取到每个方法的执行时间了,但是这样操作存在的问题也是显而易见的,shell脚本当然我相信你肯定也发现了,主要总结为以下几点:
- 代码重复、耦合度高并且看起来非常恶心
- 侵入性强
- 工作shellfish量大
那么针对这种方式的劣势,如何才能更加优雅的实现获取方法函数调用是什么意思的耗函数调用的三种方式时呢?答案就是采用AOP的方式来实现。
4.2、AOP介绍
AOP简工龄差一年工资差多少介线程池面试题:Aspect Oriented Programmin初始化电脑g,面向切面编程
- 针对同一类问题的统一处理
- 无侵入添加代码
Aspeshell什么意思ctJ简介:它就是辅助AOP用来实现切面编程
使用时首先线程池的使用需要添加如下的依赖:
//工程目录下的build.gradle
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
//app module目录下的build.gradle
apply plugin: 'android-aspectjx'
implementation 'org.aspectj:aspectjrt:1.8.14'
添加完了依赖之后,再来介shelly绍一下相关知识点,然后我们再到代码中去真正的使用它。
Join Points:程序运行时的执行点,常用的可以作为切面的地方如下所示:
- 函数调用、执行
- 获取、设置变量
- 类初始shellfish化
PointCut:带条件的JoinPoints
Advice:一种Hook,要插入代码的位置
-
Before:PointCut之前执行
-
After:PointCut之后执行
-
Around:Poishell怎么读ntCut之前、之后分别执行
-
举个栗子:
@Before("execution(* android.app.Activity.on**(..))") public void onActivityCalled(JoinPoint joinPoint) throws Throwable{ ... }
语法简介:
- Before:Advice,具体插入的位置
- execution:处理Join Point的类型,call、execution
- (* androidshellfish.app.Act工商银行ivity.on**(..)):匹配规则,匹配android.app.Activity类中任意返回值类型的on开头的是否有参数的方法都行
- on工资超过5000怎么扣税ActivityCalled:要插入的代码
代码实现:
/**
* 说明:使用AOP方式来统计方法耗时
*/
@Aspect //通过该注解,AOP框架可以知道该类即是需要需要插入的代码
public class PerformanceAop {
@Around("call(* com.jarchie.performance.app.BaseApp.**(..))") //匹配规则
public void calculateTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature(); //拿到切点签名
String name = signature.toShortString(); //拿到对应的方法信息
long time = System.currentTimeMillis();
try {
joinPoint.proceed(); //手动执行
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i("执行时间", name + "--->>>" + (System.currentTimeMillis() - time));
}
}
可以看到这里是新建龚俊了一个类采用AOP的方式来获取方法耗时,并没有在BaseApp中添加任何的代码,运行结果如下所示:
总结:
采用AOP实现:
- 无侵入性
- 修改方便
五、异步优化初始化电脑的后果
5.1、Theme切换
首先需要说明的Go是这种方式仅仅是给用户感官上的快,just a feeling,对应用真实的启动速度没有任何的影响。它的实现原理是App初始化电脑时出现问题未进行更改在打开首屏Actshellyivity之前会首先显示出一张图片,当Activity页面真正展示出来之后再把Theme改变回来,因为冷启动中有一步是创建一个空白的Window,这种实现方式正式利用了这个空白的Window。下面线程池拒绝策略来看下具体怎么操作:
首先定义一个背景drawable,这里起名为launcher.drawable:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<item android:drawable="@color/colorPrimary" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/liying" />
</item>
</layer-list>
然后在styles中定义初始化英文一个主题作为启动主题:
<style name="Theme.Splash" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launcher</item>
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
然后在首屏Activity的清单文件中设置这个主题:
<activity android:name=".MainActivity"
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
最后在首屏Activity的onCreate()方法中调用父类onCreate()方法之前将设置的启动主题改为默认主题:
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
来看下我们修改后的效果,如下图所示:
5.2、常规异步优化
核心思想:子线程分担主线程任务,并行减少时间
下面还以Applicashell编程tion的onCreate()为例分析常规的异步优化:现在的App一般情况下都是运行在八核的设备上,线程池面试题不同的设备厂商可能初始化分配给应用的核数有的四核有的八核,但是如果像我们这里的代码将所有函数调用语句的初始化工资超过5000怎么扣税工作都放在一个线程中最多占用一个核,别的三个核或者七个核都处于一线程池原理个浪费状态,那么为了让CPU的利用率达到一个更加高效的状态,这里就需要使用异步初始化了。
说到异步,那大家想到的肯定是要创建子线程了,这里使用线程池来创建线程,这种方式更加优雅,线程池的工作原理不仅可以在很大程度上避免初始化英文内存泄露,而且还可以让线程得到复用(这里线程池参数配置线程数的设置是参考了Android AsyncTask源码中的设计):
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));
@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();
LaunchTime.startRecord();
mApplication = this;
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(this::initDeviceId);
service.submit(this::initJpush);
service.submit(this::initBugly);
LaunchTime.endRecord("AppOnCreate");
}
来看下运行结果:
可以看到时间确实是非常短的,那现在有个问题:是不是以后代码都可以放在子线程中执行呢?答案当然是否定的,有些场景下并不能很好的实现异步的方案,比如:①有些代码必须要在主线程工资超过5000怎么扣税中执行;②函数调用的四个步骤有些方法必须在onCreate()方法结束后执行完毕。
针对上面这两种情况,异步的方案其实就不太好解决了,对于第一种情况你只能放弃异步方案,对于第二种情况,我们可以采用CountDownLatch这个类来解决,下面这段代码的含义大致就是:只要countDownLatch不被满足,它将一直处于等待状态,直到被满足1次,因为我们构造函数中传入的数值是1:
private CountDownLatch mCountDownLatch = new CountDownLatch(1);
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(() -> {
initBugly();
mCountDownLatch.countDown();
});
try {
mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
异步优化注意事项:
- 不符合异步要求(如果能修改成符合要求的则修改,不能修改Shell则放弃异步方案)
- 需要在某阶段完成
- 区分CPU密集型和IO密集型任务
5.3、启动器
通过上面的常规异步操作过程可以发现还是存在很多问题的,主要有以下几点:
- 代码不优雅(假如方法数较多时,则会写很多重复代码)
- 场景不好处理(特定阶段执行完毕、依赖关系)
- 维护成本高
正是因为有上面这些问题的存在,才有了下面龚俊的解决方案的产生——启动器。
推荐阿里开源的shell翻译一个启动器库alpha:github.com/alibaba/alp…
启动器介绍:
核心线程池核心参数思想:充分利用CPU多核,自动梳理任务顺序
启动器流程:
- 代码Tas初始化是什么意思k化,启动逻辑抽象为Task
- 根据所有任务依赖关系排序生成一个有向无环图(自动生成的)
- 多线程按照排序函数调用的三种方式后的优先级依次执线程池的工作原理行
启动器流程图:
代码实战:首先构建启动器初始化电脑的后果部分的代码因为这个过程还是有点复杂的,代码相对也不少,这里就不贴了,大家可以自行百度启动器相关的实现代码,这里只针对使用情况线程池的工作原理做一个说明:
首先我们需要将上shellfish面做异步操作的几个方法抽成对应的任务,比如这shell是什么意思中文里InitBuglyTask这个任务就是对应用来解决需要在特定阶段完成初始化的问题,重写needWait()方法设置为true即需要等待,并且MainTask是运行在主线程的:
public class InitBuglyTask extends MainTask {
//解决特定阶段执行完成问题
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
CrashReport.initCrashReport(mContext, "e296ad7fc8", false);
}
}
然后定义InitDeviceIdTask这个用来获取设备ID的任务,该任务是在子线程执行的:
public class InitDeviceIdTask extends Task {
private String mDeviceId;
@SuppressLint("MissingPermission")
@Override
public void run() {
//真正自己的代码
TelephonyManager tManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mDeviceId = tManager.getDeviceId();
}
}
然后函数调用中的参数太少定义初始化极光推送的任务InitJpushTask,重写shell翻译dependsOn()方法用来解决依赖关系的问函数调用中的参数太少题,该任务的执行依赖于设备ID:
public class InitJpushTask extends Task {
//解决依赖关系问题
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(InitDeviceIdTask.class);
return task;
}
@Override
public void run() {
//推送
JPushInterface.init(mContext);
}
}
然后将这些任务添加到启动器里面即可,代码看起来还是比较美观的:
LaunchTime.startRecord();
TaskDispatcher.init(this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitBuglyTask())
.addTask(new InitJpushTask())
.addTask(new InitDeviceIdTask())
.start();
dispatcher.await();
LaunchTime.endRecord("AppOnCreate");
OK,通过上面公司让员工下班发手机电量截图这几行代码启动器就搞定了,可见它相比较于传统的异步函数调用语句方式还是好处多多函数调用啊,最后来看下运行Go结果吧:
六、延迟初始化
6.1、常规方案
对于实际项目经验较多的朋友你公司让员工下班发手机电量截图会发现其实在Application或者MainActivity中有些任务它的优先级并不是很高,所以对于这类工龄差一年工资差多少任务通常都可以将它们进行延迟初初始化游戏启动器失败始化线程池的使用,一般都是延函数调用的方法迟到列表数据展示之后再进行加载。我们首先来看下常规的方案是如何实现的呢?最简单的工龄差一年工资差多少做法就是将代码移到列表显示之后进行调用,或者是通过new Handler().postDel工龄越长退休金越多吗ayed延迟Shell一个时间调用。即:
- New Handler().postDelayed
- Feed展示后调用
下面我们在代码中举个栗子说明一下这种方案是如何实现的?
这里首先定义一个回函数调用中的参数太少调函数调用的四个步骤接口是在列表展示出来之后的回调:
public interface OnFeedShowCallBack {
void onFeedShow();
}
然后在列表适线程池拒绝策略配器中定义这个接口,并给它一个setXXX()方法,并且在列表item第一条展示出来之后回调这个接口:
private OnFeedShowCallBack mCallBack;
...
public void setOnFeedShowCallBack(OnFeedShowCallBack callBack){
this.mCallBack = callBack;
}
...
if (mCallBack!=null){
mCallBack.onFeedShow();
}
接着在MainActivity的onCreate()中设置这函数调用是什么意思个回调,并函数调用可以作为一个函数的形参且让MainActivity实现回调接口重写回调方法,在回调方法中shell怎么读模拟执行两个Task,整个这个流程如果熟悉接口回调机制的兄弟应该很好理解了:
mAdapter.setOnFeedShowCallBack(this);
...
@Override
public void onFeedShow() {
//模拟执行了两个Task,TaskA和TaskB
new DispatchRunnable(new DelayInitTaskA()).run();
new DispatchRunnable(new DelayInitTaskB()).run();
}
以上就是函数调用的三种方式常规方案的实现方法,大家仔细思考一下会发现这其中是有很多问题的:首先线程池创建,我们的列表展示是发初始化生在主线程中,直接执行mCallBack.onFeedShow()方法,会跑到MainActivit函数调用可以出现在表达式中吗y重写的onFeedShow()中,如果工资超过5000怎么扣税模拟的任务执行时间较长,那么主线程就会相应的卡住对应的时长,如果此时用户滑动列表很明显会造成列表滑动卡顿,给用户的体验就很不好了。如果你采用new Handler().postDelayed发送延时消shell脚本编程100例息来处理,当然一定程度上是可以缓解这种卡段,但是函数调用语句这种方案总结下来延时的时机不太好控制并且如果任务shell脚本数量较多也不易维护,所以线程池核心参数我们需要去寻求更加优雅的解决方案。
6.2、优雅实现延迟初始化
核心思想: 对延迟任务进行分批初始化,这里利shell脚本用IdleHandler特性,空闲执行
针对这种方函数调用案我们在代码中来实践一下看看具体该如何操作?
首先来创建一个针对延迟初始化任务执行的启动器:
public class DelayInitDispatcher {
//创建任务队列
private Queue<Task> mDelayTasks = new LinkedList<>();
//IdleHandler分批处理并在系统空闲时执行
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() { //系统空闲时回调
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll(); //分批执行,每次只取一个Task
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty(); //DelayTasks为空则移除
}
};
//添加任务
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
//启动
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
具体的代码含义都加了注释了,主要就是利用了IdleHandler的特性在空闲时期执行,接着在onFeedshell翻译Show()的回调中添加任务并执行即可:
@Override
public void onFeedShow() {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new DelayInitTaskA())
.addTask(new DelayInitTaskB())
.start();
}
通过代码我们来比对一初始化失败是怎么解决下两种方案的差别:对于常规方案回调接口中有多少个任务都会一次性执行完成,也就意味着主初始化是什么意思线程会卡在那里对应的时间;对于第二种方案,我们是添加了多个任务进来,执行的时机是在系统空闲的时候进行执行,并且一次只执行一个,所以第二种线程池的工作原理方案的优点就显而易见了:
- 执行时机初始化电脑时出现问题明确;
- 有效缓解列表卡顿,它可以真正的提升用户的体验。
七、启线程池核心参数动优化其他方案
7.1、优化总方针
- 异步、延迟、懒加载(与实际业务强相关,哪里使用哪里加载)
- 技术、业务相结合
注意事项:
- wall time和cpu time的区别
- cpu time才是优化方向
- 按照systrace及cpu time跑满cpu
- 监控的完善
- 线上监控多阶段初始化电脑时出现问题未进行更改时间(App、Activity、声明周期间隔时间)
- 将监控信息上报后台,处理聚合看趋势
- 收敛启动代码修改权限
- 结合CI修改启动代码需要Review或通知
7.2、启动优化其他方案
这一部分只是简单介绍一下其他的启动优化的方案,有些方案实现起来还是比较复杂的,有需要的朋友可以查找相关资料结合自身项目实践一下。
1. 提前加载SharedPreferences:使用之龚俊前会调用getSharedPreference()方法,此时会去异步加载文件中它的配置文件xml并将它load到内存之线程池中,当我们put或者get某个属性时如果load没有完成则会阻塞一直等待
- M函数调用中的参数太少ultidex之前加载,利用此阶段CPU
- 覆写getApplicationContshell脚本ext返回this
2. 启动阶段不启动子进程
- 子进程会共Shell享CPU资源、导致函数调用可以作为独立的语句存在主进程CPU资源紧张
- 注意启动顺序:线程池有哪几种App onCreate之前是ContentProvider(启动阶段不要启动其他组件)
3. 类加载优化:提前异步类加载
- Class.forName()只加载类本身及其静态变量的引用类(需要发生在异步线程中)
- new类实例可以额外加载类成员变量的引用类
4. 启动阶段抑制GC(Native Hook)
OK,写到这里相信你已经对Android启动优化有了自己的了解了线程池创建,可能我这里介绍的不够全面,因为个人能力有限,所以对于哪些说的不够清楚的地方大家就再查找相关的资料进行更加细致的学习吧。
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙: 点赞,转工龄差一年工资差多少发,有你们的 『点赞和评论』,才是我创造的动力。微信搜索公众号 [小尘Android专栏],第一时初始化是什么意思间阅读更多干货知识!