性能优化之启动优化(一)

启动优化

对于应用的性能优化,首先我们需要了解几个概念: 首先做性能优化到底是做那些优化?实际上我看了好多网上的文章,一上来就是启动优化、内存优化,让我是一脸懵逼,你至少得告诉我性能优化是什么概念?包括哪些方面,我们一会要讲的启动优化仅仅是性能优化的一小部分,我对性能优化的理解是:真正影响用户体验,如:启动慢、稳定性、卡顿等等。比如我们知道了应用的启动总耗时时间很长,那么我们就需要有针对性的去做启动优化,我们这里还把网络优化、布局优化、绘制优化等等这些方面作为启动优化的一个点,我觉得打开以页面慢,就是让用户真正的能感受到,也算是启动的慢,所以性能优化我们要有正对性的分析,对症下药。
这篇文章只会介绍启动优化,把网络优化、布局优化、绘制优化等等分为不同的文章讲解,因为这些东西设计比较多,在介绍启动优化开始之前先介绍几个概念。

  • 冷启动
    我们先看一下冷气的流程图:
性能优化之启动优化(一)_第1张图片
冷启动.png

1、冷启动涉及的启动过程很多,包括了:IPC、创建进程、创建Application、Activity的生命周期和View的绘制,这些过程。

2、冷启动之前,会涉及到:启动APP、加载空白的Window、创建进程,这三个过程对于开发者来说其实是无法去干预的,不过加载空白的Window这一过程可以做一些操作,但是其实并不是减少了我们的启动时间。

3、冷启动之后,启动主线程(ActivityThread)、创建Application并调用生命周期方法,创建LauncherActivity并调用生命周期方法、加载布局、布置屏幕、最后就是首帧绘制,这些过程对于开发者来说是可以去干预的

  • 温启动

对于温启动,其仅仅涉及了Activity的生命周期,不会涉及到进程的创建这些过程,但是会重新加载布局和之后的过程。

  • 热启动

对于热启动来说,仅仅只是把应用从后台移到前台,这一过程,其实没有涉及到任何操作。

小结
  • 启动优化无非就是在我们应用启动过程中做了一些比较耗时的操作阻塞了主线程,其实我们做启动优化就是要减少在主线程执行的时间,这样就满足了我们对启动优化的意图,那既然这样我们是不是可以把一些耗时武安不放在工作线程?答案是看具体的需求,因为在现实的需求中有很多任务时必须要在主线程中执行的,还有一些任务是有前置和后置的,就是说task2可能需要task1的执行结果,难搞哦。

  • 对于启动优化,其实我们主要是针对冷启动去做优化的,应为冷启动包括了启动的所有过程,但是我们能干预的启动过程,不包括系统启动的过程,对于开发者来说我们能干预的就仅仅是Application和Activity的生命周期,因为它们的生命周期方法都是在主线程执行。

  • 既然我们要做启动优化,那么我们至少得知道启动的耗时时间,而这些耗时都做了什么任务?然后我们如何减少这些耗很时的任务的时间,让应用快速启动起来,其实启动优化无非就是一些耗时的任务占着主线,总的来说就是具体问题,具体分析,既然我们知道了问题就是时间,那么我们怎么测量这些时间,下里面来说一下如何测量每个任务的耗时时间和应用启动过程的总耗时时间。

应用启动时间的测量

总的来说测量启动的时间有以下几种方式:

1、 adb shell am start -W packagename/首屏Activity

adb shell am start -W com.xxx/com.xxx.ui.activity.LaunchActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xxx/.ui.activity.LaunchActivity }

Status: ok

Activity: com.xxx/.ui.activity.LaunchActivity

ThisTime: 646

TotalTime: 646

WaitTime: 681 

Complete
  • ThisTime:最后一个Activit启动耗时的时间;
  • TotalTime: 所有Activity启动耗时的时间
  • WaitTime:AMS启动Activity的总耗时时间;
  • 而ThisTime和TotalTime是有区别的,当你启动加入splashactiviy然后这季节启动MainActivity那么这两个时间就不一样了,前提是MainActivity需要在Manifest中设置export=true。

实际上他这里的时间并不是精确的时间,既然我们做优化,那肯定是从应用启动到数据展示给用户的这段时间,即我们要知道应用应用启动的耗时总时间,所以我们需要进行打点统计时间,从最早我么觉得程序运行的位置开始计时到UI渲染给用户的这段时间。

2、手动打点统计应用启动总耗时时间

启动时埋点,启动结束埋点,对二者进行差值,耗时是什么意思?无非就是在我们应用启动过程中做了一些比较耗时的操作阻塞了主线程,其实我们做启动优化就是要减少在主线程执行的时间,这样就满足了我们对启动优化的意图,那既然这样我们是不是可以把一些耗时武安不放在工作线程?答案是看具体的需求。

long sTime = System.currentTimeMillis();

.......................

Log.e("tag", name + " cost " + (System.currentTimeMillis() - sTime) + "");

怎么统计?从我们觉得程序运行最早的位置开始计时到UI渲染给用户的这段时间,是包括了网络请求这段时间的,因为我们是做优化的,当然也是包括网络这块的东西,这些过程中可能还有可能请求网络失败很多情况都要考虑。
我们知道了应用的启动总耗时时间,接下来知道这些时间到底是耗时在哪里?具体问题,具体分析。

  • 误区 onWindowFocusChanged 只是Activity的首帧绘制时间。
  • 正确 真正数据展示时间。

我们知道了应用的启动总耗时时间,那么我们就需要有针对性的去做优化,所以我们这里先做启动优化,而后可能还会有网络优化、布局优化、绘制优化等等这些方面,对应用启动过程中每一个任务的代码进行打点,然后分别做优化。

long sTime = System.currentTimeMillis();
initMap()
Log.e("tag", name + " cost " + (System.currentTimeMillis() - sTime) + "");

long sTime = System.currentTimeMillis();
initBugly()
Log.e("tag", name + " cost " + (System.currentTimeMillis() - sTime) + "");

.........几亿个初始化方法,我看你还要手动去打点?................
  • 这种手动打点的统计代码段方式,我觉得并现实,如果任务比较多,还要每个去打点,第一工作量大,第二代码侵入性强,下面来说说AOP打点。
  • 当然这些还有其他的工具,比如:traceview和systrace,可以查看具体的方法执行的耗时时间。

3、启动优化工具选择之traceview和systrace

1、traceview

  • 图形的形式展示执行时间和调用栈等等;

  • 信息全面,包含所有的线程;

    Debug.startMethodTracing("perform")

    ................

    Debug.stopMethodTracing()

  • 文件生成位置:sdcard/Android/data/packagename/files;

  • Wall Time 这段代码执行的时间;

  • Thread Time Cup执行这段代码的时间;

  • 运行时开销严重,正体都会变慢,可能会带篇我们的优化方向;

  • cpu profiler但是这个工具cpu profiler就要看手速了这也是traceview的有点吧;

2、systrace

TraceCompat.beginSection("AppCreate")
.....................
TraceCompat.endSection()

python systrace.py -b 32768 -t 5 -a com.youbesun -o performence.html sched gfx view wm am app

4、通过aspectj打点优雅获取耗时时间

先来介绍aspectj的使用

  • Join Point: 程序运行时的执行点,可以作为切面的地方,简单说就是执行点;

1、函数调用和函数执行(区别函数执行和函数调用的不一样的);

2、获取的设置变量;

3、类的初始化;

  • Point Cut

1、对于一个程序来说,在程序中时有非常多Join Point,我们需要帅选出我们感兴趣的Join Point,所以 Point Cut就来了, Point Cut其实就是带条件的Join Point,就是我们需要筛选出满足条件的点,一般我们程序的执行点是非常多的,我需要Point Cut帮助我们筛选这些执行点;

  • Advice 一种Hook要插入代码的位置,他有一下几种常用的分类:

1、Before: Join Point之前执行(比如:调用方法之前来执行);

2、After: Join Point之后执行;

3、Arount 在PointCut之前和之后执行,比如:在函数体执行之前和在函数体执行完之后执行,Before和After其实这两个测量的时间其实并不准确,经验之谈,有可能是我误操作,大家可以去试试。

  • 筛选规则:

1、excution:处理Join Point的类型:call、excution(第一个是插入函数体里面,第二个是插入到函数体外面,比如:注解)
(* android.app.Activity.on(..)) 匹配规则
第一个
代表返回值类型,..参数,on
* on开头的函数, android.app.Activity 类名

[!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]

具个栗子:

@Aspect
public class CheckPermssionAOP {
//定义切点  ---1
@Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")
public void checkPermssion(SecurityCheckAnnotation ann) {
}

@Before("checkPermssion(securityCheck)")
public void check(JoinPoint joinPoint, SecurityCheckAnnotation securityCheck) {
    //从注解信息中获取声明的权限。
    String neededPermission = securityCheck.declaredPermission();
    Log.e("tag", joinPoint.toShortString());
    Log.e("tag", "needed permission is " + neededPermission);
}
}

来看我标注的1处,这个Pointcut,首先,它在选择Jpoint的时候,@SecurityCheckAnnotation使用上了,这表明所有那些public的,并且携带有这个注解的API都是目标JPoint,接着由于我们希望在函数中获取注解的信息,所以这里的poincut函数有一个参数,参数类型是SecurityCheckAnnotation,参数名为ann这个参数我们需要在后面的advice里用上,所以pointcut还使用了@annotation(ann)这种方法来告诉AspectJ,这个ann是一个注解,然后我们从这个注解获取到我们的信息, 接下来是advice,advice的真正功能由check函数来实现,这个check函数第二个参数就是我们想要的注解。在实际运行过程中,AspectJ会把这个信息从JPoint中提出出来并传递给check函数,接下来是advice就是我们要织入代码的位置。

再举个栗子:

@Aspect
public class TraceAspect {
private static final String POINTCUT_METHOD =
        "execution(@com.wfy.aopdemo.DebugTrace * *(..))";

private static final String POINTCUT_CONSTRUCTOR =
        "execution(@com.wfy.aopdemo.DebugTrace *.new(..))";

@Pointcut(POINTCUT_METHOD)
public void methodAnnotatedWithDebugTrace() {
}

@Pointcut(POINTCUT_CONSTRUCTOR)
public void constructorAnnotatedDebugTrace() {
}

@Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String methodName = methodSignature.getName();
    long sTime = System.currentTimeMillis();
    Object result = joinPoint.proceed();
    Log.e("tag", methodName + " cost " + (System.currentTimeMillis() - sTime) + "");

    return result;
}
}

这个例子是在函数体执行前和执行后插入打印Log的AOP唯一需要注意的是构造方法的匹配规则是new。

以上Demo均参考这篇文章深入理解Android之AOP对Aspect讲的非常好。

小结

在使用Aspect时首先我们选用定义我们的Point Cut筛选出我们感兴趣的执行点,之后就可以对这些执行点进行代码的织入,对于Aspect现在应用的非常多,比如:网络检查,登录状态检查,还有就是性能监控,日志监控等等非常普遍。

开启优化之旅

Theme 切换

Theme 切换,这种方式让人重感官上,觉得该应用启动的很快,但是实际上并不是真正降低了启动的时间,前面提过应用启动时,会创建一个空白的window,然后才会到activity显示界面。

首先定义文件

  
  
  
    
  

  
  
    
  
  
  
    
  

  

然后在我们的theme中使用:


把theme设置到activity:

 
        
            

            
        
    

这样就完成了,但是需要注意的是,一定在启动完成之后再Activity的onCreate方法中切换回来

setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);

异步优化

使用子线程分担主线程任务,通过并行减少启动时间

性能优化之启动优化(一)_第2张图片
未命名文件.png

在图中可以看到,主线程中的任务被分为n个线程并行执行,从而减少了主线程中的时间。
首先来看看下面的代码:

private val mCountDownLatch by lazy { CountDownLatch(1) }


val poolExecutor = DefaultPoolExecutor.getInstance()
    //将任务放到线程池中执行    
    poolExecutor?.submit { mApplicationDelegate.onCreateAsync(this) }
    poolExecutor?.submit { StabilityHelper.initBUGLY(this, BuildConfig.DEBUG) }
    poolExecutor?.submit {
        initARouter()
        mCountDownLatch.countDown()
    }
    //等ARouter初始化完成,因为首页需要注入
    tryCatch({ mCountDownLatch.await() })
    WebViewHelper.preloadWebView(this)

异步任务中我用到了CountDownLatch来控制,有些任务会依赖于其他的任务,比如ARouter的初始化,我们在首页会使用到ARouter,那么如果ARouter没有初始化完成就会造成崩溃。细心的同学可能会发现WebViewHelper.preloadWebView(this)这行代码,这行代码是对WebView做预加载的操作,也是耗时的操作:

 fun preloadWebView(application: Application) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        //将在系统闲置的时候执行
        application.mainLooper.queue.addIdleHandler {
            startChromiumEngine()
            false//返回false将会移除这个IdleHandler
        }
    }
}

这里用到IdleHandler对WebView做了延迟加载,对IdleHandler不清楚的同学可以去看看源码,IdleHandler就是当系统闲置时会执行,不会影响到我们的程序。

你可能感兴趣的:(性能优化之启动优化(一))