一线大厂资深APP性能优化系列-启动优化总结(五)

4,559 阅读8分钟

1.前言

最近感觉真的很懒惰,答应几个小伙伴的更新,也迟迟没更,今天给补上。这一章主要是对前4章学习的总结

一线大厂资深APP性能优化系列-卡顿定位(一)

一线大厂资深APP性能优化系列-异步优化与拓扑排序(二)

一线大厂大型APP性能优化系列-自定义启动器(三)

一线大厂大型APP性能优化系列-更优雅的延迟方案(四)

这四章其实也是启动优化这一个大章节的内容,看完这4个,至少启动优化这个地方就已经很OK了。 当然接下来我们会进入 第二大章节的内容学习了,第二大章节,预计有5个小章节组成,是全套介绍在一个真实项目中的如何进行内存优化 希望大家能提前先了解下关于内存优化这方面的知识。

当然了,之前也吐槽过,学习一定要扎实,很多人对于内存优化这方面的内容的理解就是:LeakCanary的使用及原理,但是检测出来如何优化?如何避免这些问题,这就是经验了


2.卡顿定位总结

这是我们第一大章中的第一小章,具体看

一线大厂资深APP性能优化系列-卡顿定位(一)

在这一章我们主要讲的就是如何获取方法耗时,工欲善其事,必先利其器,这也是日常我们进行大项目优化的第一步,先分析一下这个大项目的耗时情况,有很多同学反映说是进行老项目维护升级,卡顿无从下手,所以分析卡顿的耗时时间就是进行项目优化的第一步。

1.常规方式

直接在每个方法上下各加入一行代码,然后打印,如:

public void func(){
    long startTime = System.currentTimeMillis();
     xxxx
    Log.d("lybj", "func()方法耗时:"+ (System.currentTimeMillis() - startTime));
}

简单是简单,但是记住我们做性能检测的,是不能写这种入侵性很强的代码,很有可能本来很稳定的系统,因为你的性能检测的代码,变成了不稳定,这是不被允许的。所以废弃!

2.Aop方式

这种方式尼,可以使得我们的性能检测代码与逻辑代码相分离

@Aspect
public class PerformanceAop {

    @Around("call(* com.bj.performance.MyApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint){

        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.d("lybj", methodName + "方法耗时:"+ (System.currentTimeMillis() - startTime));
    }
}

缺点: 如果项目比较庞大,上百个方法,总不能全部打点,然后一个一个的分析到底是哪个地方运行时间过长吧,所以我们需要一个比较直观的工具,一眼就能看到具体哪个方法运行时间过长。

3.wall time 与 cpu time

在traceView里面的时间模式有两种:

wall time 就是执行这段代码的时间,但是如果该线程被卡住了,等待的时间也是被算在内的。

cpu time(Thread Time) 则是CPU具体花在这个线程上的时间,它的时间也一定小于wall time的时间,

所以如果发现wall time 时间很长,cpu time时间很短,那么就说明了一件事,你需要开异步线程了,因为耗时的主要原因都是,CPU在闲置,而你的线程在等待。


3.异步优化总结

启动优化的开端,对application里面初始化的第三方依赖进行优化,这个涉及到两章内容,如下:

一线大厂资深APP性能优化系列-异步优化与拓扑排序(二)

一线大厂大型APP性能优化系列-自定义启动器(三)

1.常规方式

可以使用线程池或者开启线程去实现,目的很简单,为了帮主线程分担压力。

缺点:1.如果两个异步之间执行的内容有着依赖关系,则不好处理

比如我们要初始化极光推送,还要获取设备ID,假如这两个都是耗时方法,均需要在子线程中进行初始化,但是必须得先获取设备ID,再初始化极光推送,两者之间存在依赖关系,则不是很好处理了,具体查看第二章节内容。

缺点:2.如果其中一个异步处理,需要先执行完毕自己,主线程才能继续往下执行,那么常规方式也不好处理。

缺点:3.如果上面两个你通过同步锁机制都处理了,那么恭喜你,你的代码可读性肯定不高!

2.启动器原理

还记得吗,咱们一起实现了个启动器,原理很简单,具体看第三章,这里只是简述:

task

首先是将我们的耗时操作均封装成一个task,它有4个方法1个属性

属性1 :taskCountDownLatch,该task有几把锁尼,取决于它的依赖的task集合数。
方法1 :dependentArr()返回它所依赖的task集合
方法2 :startLock()开启锁
方法3 :unlock()执行完一个依赖,减少一把锁
方法4 :needWait() 是否需要主线程等你执行完再执行

启动器(TaskManager)

启动器主要是用来分发task的

属性1:mCountDownLatch 锁,不同于task里的锁,这个锁是锁主线程的,当主线程调用它时,它会锁定,锁的数量取决于等待计数器(mainNeedWaitCount)的数量

方法1:add: 将task添加至集合中,并以需要依赖的对象为key,集合为value创建一个Map,这个Map是做什么的尼?主要啊,当一个task运行完毕,就可以循环这个Map找到这个运行完毕的task,获取到需要依赖它的对象集合,然后让每一个对象,均减少一把锁。然后调用这个task的needWait()方法,看看是否需要主线程等待它,如果需要,等待计数器+1

方法2:startTask:分发task,首先是根据有向无环图的拓扑排序,将task集合重新排序,比如,我们传入的任务是A,B,C但是尼,如果A依赖于B,那么就需要先初始化B,同时处理C,然后再处理A。返回就是B、C、A。设置主线程的锁,锁的数量等待集合数量,保证一些要切换页面之前必须要执行完的task均执行完。然后根据所要求的线程进行分发处理。

执行器(TaskRunnable)

执行器主要就是为了执行task的,它继承了Runnable

方法1:run(): 启动task里面的锁,还记得不,它的锁的数量取决于它所依赖的对象数,目的就是先执行完它的依赖。然后尼,执行task里面的run方法,这个是个空方法,用于执行我们的耗时任务,然后执行启动器的unLockForChildren方法,还记得吗,就是用来循环之前存的Map找到这个运行完毕的task,获取到需要依赖它的对象集合,然后让每一个对象,均减少一把锁。

是不是很简单?不理解的就去研究第三章内容,熟读并背诵


4.更加优雅的异步加载

涉及到一章内容,如下:

一线大厂大型APP性能优化系列-更优雅的延迟方案(四)

1.常规方式

可以通过Handler().sendMessageDelayed() 达到延迟加载

但是项目中是不建议这样用的,因为会抢占CPU,性能会进行耗损,比如一个页面的一些第三方服务进行初始化操作,虽然说是可以延迟一段时间再去初始化,但是如果该页面有任务一直在执行,比如有个定时器或者轮询请求接口等,那么延时的时间到了,依然是要抢占CPU来执行我们的第三方服务的初始化操作。所以不能直接这么用

2.IldeHandler

这个是没问题的,可以在CPU空闲时间进行处理耗时操作,但是如果加入的任务存在着先后执行顺序等,就无法单纯的使用它了,因为它是无序的,加入的任务是谁有空执行谁

3.异步启动器

具体看第四章节内容,这里只是简介

IldeTaskManager

原理很简单,有一个方法add()作用就是把task添加到集合里面,然后封装一个IldeHandler,遍历这个集合,取得task后,交给TaskRunnable去执行它,TaskRunnable就是我们第三章节时封装的。


5.源码

好了,这里有大家最期待的源码

点击下载 记得点个星!


6.END

愉快的假期生活完事了,公司要求复工了,真是一个悲伤的故事。

经过了一个月,总算是把第一大章节启动优化的内容更新完了,基本也就这些内容,在网上查了查虽然还有很多,比如预加载字节码文件等,但是尼,其实感觉没必要,一个软件快了0.1秒其实是作用不大的,还有的说是锁定CPU的频率,但是你就无法保证节约电量的作用,当然还有些适配方案,但是经过验证,要么没什么效果,要么有些华而不实。基本上掌握了上述的4个章节,启动优化就够了,即便是大厂优化到这个程度也是可以了。

接下来的几周会给大家更新第二大章节的内容,涉及的是在实战项目中关于内存的优化方案。大家记得要复习一下!

最后:欢迎大家 点赞+关注,你们的支持是我继续更新完下面 16-17 章内容的动力!