经过上期的学习(一线大厂资深APP性能优化系列-卡顿定位(一)),咱们学会了 定位及获取程序的耗费时间 并找到卡顿的地方。这期咱们来谈谈具体的优化方案,首先是 异步优化 html
异步优化的核心思想:子线程来分担主线程的任务,并减小运行时间java
接着上期的内容,经过卡顿定位,找到咱们卡顿处的代码node
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
initBugly();
initBaiduMap();
initJPushInterface();
initShareSDK();
// Debug.stopMethodTracing();
}
private void initBugly() throws InterruptedException {
initBaiduMap();
Thread.sleep(1000);
}
private void initBaiduMap() throws InterruptedException {
Thread.sleep(2000);
}
private void initJPushInterface() throws InterruptedException {
Thread.sleep(3000);
}
private void initShareSDK() throws InterruptedException {
Thread.sleep(500);
}
}
复制代码
执行完毕打开页面大约是 5.5秒,太慢了,须要优化!git
启动优化是一个大型软件开发中要作的第一步优化,由于不管你的APP作的内容有多么丰富,若是启动比较慢的话,那么给用户的第一印象就会很是很差。github
想到异步首先想到的就是开线程,可是须要注意的是不要直接就去开线程,由于线程缺少统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源致使死机或oom,因此咱们使用线程池的方式去执行异步。算法
对线程池不了解的就去看 大话线程池原理性能优化
这里咱们采用 FixedThreadPool 来解决卡顿。可是问题来了, 这个线程池须要指定多少个线程合适尼? bash
有人会说,有几个方法就指定几个呗。。(忽然想吐槽下,阿里里面曾经有个项目,就是这么作的,开的线程池就是按心情随便指定了个数字。。。实际上是不对的,但咱人小势微的也不敢说啊)并发
为了更高效的利用CPU,尽可能线程池数量不要写死,由于不一样的手机厂商的CPU核数也是不同的,设置的线程数太小的话,有的CPU是浪费的,设置过多的话,均抢占CUP,也会增长负担。因此正确的写法是:框架
// 得到当前CPU的核心数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 设置线程池的核心线程数2-4之间,可是取决于CPU核数
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
// 建立线程池
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
复制代码
实验证实,根据这个公式来算出的线程数是最合理的,至于为何是这样算,不是全占用了CPU数是最好的吗?
表示不信做者的同窗你就本身看下面的公式,欢迎来怼做者!
好了咱们用线程池改造下
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
executorService.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initBaiduMap();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initJPushInterface();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initShareSDK();
}
});
// Debug.stopMethodTracing();
}
复制代码
通过测试,如今打开页面基本是秒开的。
可是又出现了问题,好比有的是必需要先执行完毕才能进入页面的。也就是说,得先让它执行完毕,才能进入主页。
很简单-加锁就行,关于锁的介绍就不在这里赘述了,有不少,可是做者用的是CountDownLatch, 又称之为门栓,构造函数就是指定门栓的个数, latch.countDown(); 每次调用都减小一个门栓数, latch.await(); 就是开启等待,当门栓数为0时,就放开去执行下面的逻辑。
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
final CountDownLatch latch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
executorService.submit(new Runnable() {
@Override
public void run() {
initBugly();
latch.countDown();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initBaiduMap();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initJPushInterface();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initShareSDK();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Debug.stopMethodTracing();
}
复制代码
这样就好了,只有当initBugly执行完毕才能继续跳转页面,固然值得补充的是,之因此要加门栓,是由于在onCreate方法里面,可能还有其余必须在主线程才能初始化的其余耗时任务,而initBugly能够不须要在主线程里面初始化,可是又必须得初始化完毕才能跳转页面。因此为了避免再增长时间,才启动线程池+门栓去初始化
好了,既加快了速度,又能够保证一些不须要在主线程优化而又启动以前必须初始化完成的任务不出问题,
可是尼,这么写,有点Low,并且若是有的耗时方法存在关联,好比必须先执行完A,根据A的返回值,再执行B,而后根据B的返回再执行C,那么必须串联的话,咱们该如何优化尼?
先看图,目的就是进行任务分类
看了图,是否是略微明白了?咱们的任务基本就是这样的,有的必需要在因此任务以前初始化,有的必需要在主线程初始化,有的能够有空在初始化,有的必需要在有的任务执行完毕再初始化,好比激光推送须要设备ID,那么就必需要在获取设备ID这个方法执行完才能执行,因此咱们要对耗时任务先分类。
因而有了上图
好了分好类了以后,咱们发现若是用常规的方式去作,好比线程池+门栓就很麻烦了,并且效率不高,为了极致的性能体验,咱们会本身作一个启动器
先看下咱们完善后的代码
// 使用启动器的方式
TaskDispatcher.init(this);
TaskDispatcher instance = TaskDispatcher.createInstance();
instance.addTask(new initBuglyTask()) // 默认添加,并发处理
instance.addTask(new initBaiduMapTask()) // 在这里须要先处理了另一个耗时任务initShareSDK,才能再处理它
instance.addTask(new initJPushInterface()) // 等待主线程处理完毕,再进行执行
.start();
instance.await();
复制代码
看无语伦比的简单,不管是须要别的任务执行完再执行的继承关系,仍是必须主线程执行完的等待,仍是能够并发的执行,在定义task里面,只须要一个方法便可。
什么是启动器?为啥用它?
在应用启动的时候,咱们一般会有不少工做须要作,为了提升启动速度,咱们会尽量让这些工做并发进行。但这些工做之间可能存在先后依赖的关系,因此咱们又须要想办法保证他们执行顺序的正确性,很麻烦。
虽然阿里也出了个启动器 alibaba / alpha
可是尼,在狗东用阿里的框架总感受怪怪的,接下来的时间就带领你们去打造一款属于本身的启动器。
要想作启动器,首先是要解决一些依赖关系,好比,咱们传入的任务是A,B,C可是尼,若是A依赖于B,那么就须要先初始化B,同时处理C,而后再处理A。
怎么排序尼,这个有个专业的名称,叫作了 任务的有向无环图的拓扑排序
不知道的去看这里 有向无环图的拓扑排序
好了,我先贴出算法的代码,有个简单的认识即可,下期带领你们一步一步的打架本身的启动器。
TaskSortUtil.java
public class TaskSortUtil {
private static List<Task> sNewTasksHigh = new ArrayList<>();// 高优先级的Task
/** * 任务的有向无环图的拓扑排序 * * @return */
public static synchronized List<Task> getSortResult(List<Task> originTasks, List<Class<? extends Task>> clsLaunchTasks) {
long makeTime = System.currentTimeMillis();
Set<Integer> dependSet = new ArraySet<>();
Graph graph = new Graph(originTasks.size());
for (int i = 0; i < originTasks.size(); i++) {
Task task = originTasks.get(i);
if (task.isSend() || task.dependsOn() == null || task.dependsOn().size() == 0) {
continue;
}
for (Class cls : task.dependsOn()) {
int indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls);
if (indexOfDepend < 0) {
throw new IllegalStateException(task.getClass().getSimpleName() +
" depends on " + cls.getSimpleName() + " can not be found in task list ");
}
dependSet.add(indexOfDepend);
graph.addEdge(indexOfDepend, i);
}
}
List<Integer> indexList = graph.topologicalSort();
List<Task> newTasksAll = getResultTasks(originTasks, dependSet, indexList);
DispatcherLog.i("task analyse cost makeTime " + (System.currentTimeMillis() - makeTime));
printAllTaskName(newTasksAll);
return newTasksAll;
}
@NonNull
private static List<Task> getResultTasks(List<Task> originTasks, Set<Integer> dependSet, List<Integer> indexList) {
List<Task> newTasksAll = new ArrayList<>(originTasks.size());
List<Task> newTasksDepended = new ArrayList<>();// 被别人依赖的
List<Task> newTasksWithOutDepend = new ArrayList<>();// 没有依赖的
List<Task> newTasksRunAsSoon = new ArrayList<>();// 须要提高本身优先级的,先执行(这个先是相对于没有依赖的先)
for (int index : indexList) {
if (dependSet.contains(index)) {
newTasksDepended.add(originTasks.get(index));
} else {
Task task = originTasks.get(index);
if (task.needRunAsSoon()) {
newTasksRunAsSoon.add(task);
} else {
newTasksWithOutDepend.add(task);
}
}
}
// 顺序:被别人依赖的—》须要提高本身优先级的—》须要被等待的—》没有依赖的
sNewTasksHigh.addAll(newTasksDepended);
sNewTasksHigh.addAll(newTasksRunAsSoon);
newTasksAll.addAll(sNewTasksHigh);
newTasksAll.addAll(newTasksWithOutDepend);
return newTasksAll;
}
private static void printAllTaskName(List<Task> newTasksAll) {
if (true) {
return;
}
for (Task task : newTasksAll) {
DispatcherLog.i(task.getClass().getSimpleName());
}
}
public static List<Task> getTasksHigh() {
return sNewTasksHigh;
}
/** * 获取任务在任务列表中的index * * @return */
private static int getIndexOfTask(List<Task> originTasks, List<Class<? extends Task>> clsLaunchTasks, Class cls) {
int index = clsLaunchTasks.indexOf(cls);
if (index >= 0) {
return index;
}
// 仅仅是保护性代码
final int size = originTasks.size();
for (int i = 0; i < size; i++) {
if (cls.getSimpleName().equals(originTasks.get(i).getClass().getSimpleName())) {
return i;
}
}
return index;
}
}
复制代码
Graph.java
/** * 有向无环图的拓扑排序算法 */
public class Graph {
//顶点数
private int mVerticeCount;
//邻接表
private List<Integer>[] mAdj;
public Graph(int verticeCount) {
this.mVerticeCount = verticeCount;
mAdj = new ArrayList[mVerticeCount];
for (int i = 0; i < mVerticeCount; i++) {
mAdj[i] = new ArrayList<Integer>();
}
}
/** * 添加边 * */
public void addEdge(int u, int v) {
mAdj[u].add(v);
}
/** * 拓扑排序 */
public Vector<Integer> topologicalSort() {
int indegree[] = new int[mVerticeCount];
for (int i = 0; i < mVerticeCount; i++) {//初始化全部点的入度数量
ArrayList<Integer> temp = (ArrayList<Integer>) mAdj[i];
for (int node : temp) {
indegree[node]++;
}
}
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < mVerticeCount; i++) {//找出全部入度为0的点
if (indegree[i] == 0) {
queue.add(i);
}
}
int cnt = 0;
Vector<Integer> topOrder = new Vector<Integer>();
while (!queue.isEmpty()) {
int u = queue.poll();
topOrder.add(u);
for (int node : mAdj[u]) {//找到该点(入度为0)的全部邻接点
if (--indegree[node] == 0) {//把这个点的入度减一,若是入度变成了0,那么添加到入度0的队列里
queue.add(node);
}
}
cnt++;
}
if (cnt != mVerticeCount) {//检查是否有环,理论上拿出来的点的次数和点的数量应该一致,若是不一致,说明有环
throw new IllegalStateException("Exists a cycle in the graph");
}
return topOrder;
}
}
复制代码
网上有不少这种类型的代码,百度便可
好了,原本打算想把启动器全写在这一章节里面,可是吧,这周就放了一天,休息时间太少了,只能把一章的内容拆为2章,有兴趣的小伙伴记得点赞加关注,下期关于自定义启动器的章节,大概3天后,若是不忙的话,更新出来。
这章基本就是认识下不要写死线程池,那并非最优解,并且Application里面或者Activity的启动项,是能够按照上面的图片进行分别分类的。可是普通方法执行起来实在太麻烦。关注下一期的启动器章节,end!