启动优化

启动优化

对于应用的性能优化,首先我们需要了解几个概念:

  • 首先做性能优化到底是做哪些优化?我的理解是:真正影响用户体验,如:启动慢、稳定性、卡顿,网络慢等等。如:我们知道了应用的启动总耗时时间很长,那么是什么造成App启动这么长,用户等的花儿都谢了。

  • 对于开发人员来说,有哪些因素会影响启动速度?我的理解是,网络慢、布局加载太久了、绘制耗时等等这些方面作为启动优化的一个点,我觉得让用户真正的能感受到而言打开页面慢也算是启动的慢,所以性能优化我们要有正对性的分析,回归于用户,并不是上来我要做启动优化,那么最后我们也是会把网络优化、布局优化、绘制优化当成启动优化的一部分了。

冷启动

做启动优化那么首先我们应该先了解App是如何启动的大致过程流程图:


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

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

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

  • 4、如通过ADB查看启动耗时:

    adb shell am start -W com.github.gradle.baidu/com.github.gradle.AptActivity    
    Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
    cmp=com.github.gradle.baidu/com.github.gradle.AptActivity }
    Status: ok
    LaunchState: COLD  -----------冷启动
    Activity: com.github.gradle.baidu/com.github.gradle.AptActivity
    TotalTime: 769
    WaitTime: 773
    Complete
    
  • 温启动,如通过ADB查看启动耗时:

    adb shell am start -W com.github.gradle.baidu/com.github.gradle.AptActivity
    Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]     
    cmp=com.github.gradle.baidu/com.github.gradle.AptActivity }
    Status: ok
    LaunchState: WARM  --------------温启动
    Activity: com.github.gradle.baidu/com.github.gradle.AptActivity
    TotalTime: 191
    WaitTime: 196
    Complete
    

对于温启动,Activity是已经被销毁了,但是App进程并没有销毁,所以启动Activity仅仅涉及了Activity的生命周期并不会重新创建App进程,仅仅只是把App从后台切到前台,不会涉及到App进程的创建这些过程,但是会重新加载布局和之后的过程

  • 热启动,如通过ADB查看启动耗时:

    adb shell am start -W com.github.gradle.baidu/com.github.gradle.AptActivity
    Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]     
    cmp=com.github.gradle.baidu/com.github.gradle.AptActivity }
    Warning: Activity not started, its current task has been brought to the front  -- 这句话就已经说明了
    Status: ok
    LaunchState: HOT   ------- 热启动
    Activity: com.github.gradle.baidu/com.github.gradle.AptActivity
    TotalTime: 134
    WaitTime: 138
    Complete
    

对于热启动来说Activity并没有销毁,只是Activity Task被某种原因移动到了后台,当启动时仅仅时将Activity Task移动到前台,这一过程,其实没有涉及到任何操作,

小结
  • 启动优化无非就是在我们应用启动过程中做了一些比较耗时的操作阻塞了主线程,其实我们做启动优化就是要减少在主线程执行的时间,这样就满足了我们对启动优化的意图,那既然这样我们是不是可以把一些耗时武安不放在工作线程?答案是看具体的需求,因为在现实的需求中有很多任务时必须要在主线程中执行的,还有一些任务是有前置和后置的,就是说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(call是将代码织入到函数调用处,excution是将AOP代理代码织入到函数体里)
(* 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显示界面。

首先定义文件

  <layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
  <!-- 设置整个屏幕背景为白色 -->
  <item >
    <color android:color="@color/white"/>
  </item>

  <!-- 中间logo -->
  <item >
    <bitmap
        android:gravity="center"
        android:src="@drawable/ic_launcher" />
  </item>
  <!-- 底部图表 -->
  <item android:bottom="10dp">
    <bitmap
        android:gravity="bottom|center_horizontal"
        android:src="@drawable/copyright" />
  </item>

  </layer-list>

然后在我们的theme中使用:

<style name="StartAppTheme" parent="AppBaseTheme">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@drawable/layout_list_start_pic</item>
    <!-- All customizations that are NOT specific to a particular API-level can go here. -->
</style>

把theme设置到activity:

 <activity
        android:name=".LauncherActivity"
        android:label="@string/app_name"
        android:theme="@style/StartAppTheme" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

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

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

异步优化

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

未命名文件.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就是当系统闲置时会执行,不会影响到我们的程序。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容

  • java 接口的意义-百度 规范、扩展、回调 抽象类的意义-乐视 为其子类提供一个公共的类型封装子类中得重复内容定...
    交流电1582阅读 2,240评论 0 11
  • 一、前言 随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我...
    Android高级开发阅读 1,529评论 0 10
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,107评论 1 32
  • 这一季初夏刚刚开始,我便在初夏的第一个工作日下班回家。到家已经是傍晚时分,夕阳染透了半边天。走在回家的那条短而宽广...
    诗韵钟鸣阅读 260评论 0 2
  • 生命始于两个游走的小生物,在对的时间遇到对的人,然后结合在一起!这是一个十分坚韧的小精灵,他能冲破万难,汲取...
    小娜说说阅读 186评论 0 0