Android性能优化系列一:启动优化

本文详细介绍了Android应用的启动优化,包括黑白屏优化、冷/热/暖启动的区别、App启动时间检测方法以及启动优化工具的选择。针对代码未优化造成的问题,提出了手动打点和使用traceview、Systrace等工具进行检测。文章还探讨了异步优化策略,如使用线程池和启动器来提高启动效率,并提供了一个更优秀的延迟初始化方案,以在不影响用户体验的情况下进行任务执行。
摘要由CSDN通过智能技术生成

黑白屏优化

黑白屏原因

​ 在App的启动流程中,我们已知:当系统加载并启动App时,需要消耗相应的时间,即使不到1s,用户也会感觉到当点击App图标时会有“延迟”现象,为了解决这一问题,Google的做法是在App创建的过程中,先展示一个空白页面(实际上就是窗体的默认背景, 启动页面的UI加载绘制完成后显示xml布局内容),就是为了让用户体会到点击图标之后立马就有响应;而这个空白页面的颜色则是根据我们在manifest文件中配置的主题背景色来决定的;一般默认是白色。

解决方案:

方案1:修改AppTheme

​ 在应用默认的AppTheme中,设置系统"取消预览(空白窗体)"为true,或者设置空白窗体为透明;具体代码如下;该两种方式都属于同一种方案:将Theme的背景改为透明,这样用户从视觉上就无法看出黑白屏的存在。简单粗暴的取消了一点击就有响应的初衷,于是在保留google初衷的前提下,考虑其他方案。

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    <!-- Customize your theme here. -->    
    <item name="colorPrimary">@color/colorPrimary</item>    
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>    
    <item name="colorAccent">@color/colorAccent</item>    
    <!--设置系统的取消预览(空白窗口)true-->    
    <item name="android:windowDisablePreview">true</item>    
    <!--设置背景为透明-->    
    <item name="android:windowIsTranslucent">true</item>
</style>

方案2:

  1. 自定义继承自AppTheme的主题
  2. 将启动Activity的theme设置为自定义主题
  3. 在启动Activity的onCreate方法中,在super.onCreate和setContentView方法之前调用setTheme方法,将主题设置为最初的AppTheme,(这一步根据自己的实际情况决定是否添加)
<!-- 1.自定义主题 -->
<style name="AppTheme.LaunchTheme"> 
    <!-- 设置背景图片 -->
    <item name="android:windowBackround">@drawable/lauch_layout</item>
    <!-- 设置全屏 -->
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowNoTitle">true</item>
</style>
<!-- 2.设置启动Activity主题 -->   
<activity
    android:name=".SplashActivity"
    android:theme="@style/AppTheme.LaunchTheme">
        <intent-filter>
        	<action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
</activity>
<!--3.在代码中将主题设置回来 -->     
protected void onCreate(Bundle savedInstanceState) {
   
	setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);     
    setContentView(R.layout.activity_splash);
}

冷/热/暖启动

  • 冷启动:程序从头开始,系统没有为该应用程序创建进程。一般场景:程序安装后第一次启动;
    应用程序被系统完全终止后再打开。

  • 热启动:此时程序仍然驻留在内存中,只是被系统从后台带到前台,因此程序可以避免重复对象初始化,加载布局和渲染。需要注意的是,如果程序的某些内存被系统清除,比如调用了onTrimMemeory方法,则需要重新创建这些对象以响应热启动事件

  • 暖启动/温启动:它包含热启动和冷启动一系列的操作子集,比热启动的消耗稍微多一点。它与热启动最大的区别在于,它必须通过调用onCreate方法开始重新创建活动,也可以从传递给onCreate方法中保存的实例状态中获得某些对象的恢复。

冷启动流程:加载并启动App—>启动后立即为该App显示一个空白启动窗口—>创建App进程(创建应用程序对象)—>创建主Activity—>加载布局、绘制

有了三种启动的基本认识,那我们启动优化的重点应该关注哪一种启动方式呢?显然是冷启动,它做的事情做多,也最耗时,生命周期方法也调用的最多,由于进程的启动的过程,部分流程对我们来说就像黑匣子,无法针对性的做相关处理,所以我们能想到的的重点就是放在我们能够触及的地方,Application的onCreate和启动页LaunchActivity 的onCreate方法。

代码未优化造成的问题

​ 在构建App时,我们经常需要引用一些第三方SDK,而项目业务越多,引用的第三方也越多,有些第三方会要求在Application的onCreate方法中对其进行初始化。这意味着:在application的onCreate方法中执行的时间会越长,首个Activity布局渲染时间也会相应的拉长。同理,如果我们在Activity的onCreate,onStart、onResume方法中执行任务时间过长,同样也会导致布局被渲染的时间拉长。这样直接导致的问题就是,用户会感觉页面迟迟没有加载出来,大大降低了用户体验。

有了大致的优化方向,我们需要知道启动过程的耗时情况,如何检测App启动的时间呢?有了启动时间的监测
结果,才能根据优化前后的结果对比,具体分析优化效果。

App启动时间检测方式

方式一:adb命令

adb shell am start -W packageName/packageName.MianActivity(首屏Activity)

ThisTime : 最后一个 Activity 的启动耗时(例如从 LaunchActivity - >MainActivity「adb命令输入的Activity」 , 只是统计 adb命令输入的Activity的启动耗时,它之前可能还有SplashActivity、GuideActivity)

TotalTime : 所有Activity的启动总耗时(有几个Activity 就统计几个)

WaitTime : 应用进程的创建过程 + TotalTime .

ThisTime<TotalTime<WaitTime

优点:线下使用方便

缺点:不能带到线上使用,非严谨、精确时间

方式二:手动打点

启动时埋点,启动结束埋点,二者差值就是启动时间。我们可以定义一个专门来处理启动时间检测的类

public class LauncheTimer{
   
    private static long sTime;
    // 记录启动开始时间
    public static void startRecord() {
   
        sTime = System.currentTimeMillis();
    }
    // 记录启动结束时间
    public static void endRecord(String string) {
   
        long cost = System.currentTimeMillis() - sTime;
        Log.d(string , "cost "+ cost);
    }
    
}

​ 我们能够接触最早的启动相关回调方法就是Application的attachBaseContext方法,在该方法中调用LaunchTimer的startRecord();那么问题来了,在哪里调用启动结束的方法呢?网上不少的资料都是调用onWindowFocusChanged方法作为启动的结束时间点。其实这是一个误区,这个方法调用的时间点仅仅是Activity的首帧时间,即首次进行绘制的时间,并不能表示界面已经真正的展示出来了,性能优化的核心是为了提升用户体验,并不是仅仅在乎数据的好看(这里指的是启动时间表面上的缩短),当然这只是目的之一,所以启动过程开始和结束的埋点就比较重要,应该是应用真实启动结束时即用户真正看到界面时,而不应该只是首帧绘制时间点。正确的做法是,等到真实数据展示,再调用记录启动结束时间的方法endRecord,通常是Feed第一条展示,主页列表展示的第一条。

@Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
   
        if (position == 0 && !mHasRecorded) {
   
            mHasRecorded = true;
            holder.layout.getViewTreeObserver().addOnPreDrawListener(new 				ViewTreeObserver.OnPreDrawListener() {
   
                 @Override
                 public boolean onPreDraw() {
   
             holder.layout.getViewTreeObserver().removeOnPreDrawListener(this);
                            LaunchTimer.endRecord("FeedShow");
                            return true;
                        }
                    });
        }

​ 手动埋点方式可精确的控制开始的位置和结束的位置,可以方便的进行用户启数据的采集,上报给服务器,针对所有的用户上报的数据进行整合,算平均值,根据不同版本进行比对。但是要注意避开埋点误区,采用Feed第一条展示作为启动结束点。另外addOnDrawListener要求API最低版本为16

启动优化工具选择

​ 常用的就是systrace和traceview这两种工具,两种方式互相补充,所以需要正确认识工具及不同场景选择合适的工具(才能发挥工具最大作用)。我们先来了解traceview这个工具

traceview介绍

traceview的特点:

  • 图形的形式展示执行时间、调用栈等
  • 信息全面,包含所有线程

使用方式:

  • Debug.startMethodTracing(“文件名”);// 开始使用traceview
  • Debug.stopMethodTracing();// 结束,并生成文件记录相关信息
  • 生成文件在sd卡:Android/data/packageName/files

如果是采用真机调试,通过adb pull /storage/emulated/0/文件名.trace 将文件拉到电脑的项目目录。拖进as就可以进行分析了。这不是本文重点受篇幅所限,这里提一下分析的注意事项,至于具体使用分析可自行百度。

Top Down 展示函数调用列表如函数A调用了哪些函数,依次展示出来,函数右键Jump to Source 跳转至源码处

  • Wall Clock Time 代码在线线程执行,线程真正执行的时间
  • Thread Time: CPU执行的时间,比Wall Clock Time 小
  • Self: 函数自身所用的时间
  • Children:子函数所用的时间
  • Total:函数用的总时间(Total = Self + Children)

需要注意Wall Clock Time和Thread Time的区别,有可能没有获取到锁,线程处于阻塞等待锁,这也是耗时间的,获取到锁后CPU执行任务的时间才是Thread Time,所以说Wall Clock Time大于Thread Time,而Thread Time才是性能优化应该关注的指标,否则容易误导优化方向

Call Chart :函数A调用函数B,A在上面,B在下面

  • 橙色:系统API调用
  • 绿色:应用自身函数的调用
  • 蓝色:第三方API调用

traceview缺点:

  • 运行时开销严重,整体速度会变慢:因为功能强大,会抓取当前所有运行的线程的,所有执行函数的顺序和耗时
  • 可能会带偏优化方向:因为traceview会导致程序变慢,所以可能导致某些原本不耗时的函数,现在耗时比原来有明显的变长,让人错误的认为这些函数耗时严重,从而带偏优化方向。
  • traceview和cpu Profiler:我们要根据工具的特点来选择,如果打开App,直接通过Profiler来抓取启动过程的堆栈信息,需要操作手速和启动速度高度一致,这显然是不可能的。而traceview可以手动埋点,就可以有效的避免这个问题。
Systrace介绍

Systrace特点

结合Android内核的数据,生成Html报告

API18以上使用,推荐TraceCompat向下兼容

使用方式

  • TraceCompat.beginSection(“xxx”);// 手动埋点起始点

  • TraceCompat.endSection();// 手动埋点结束点

  • python systrace.py -b 32768 -t 5 -a packageName -o trace.html sched gfx view wm am app

优势:

  • 轻量级,开销小

  • 直观反映cpu利用率

Systrace具体使用不是本文重点,有兴趣的可参考: Android应用开发性能优化完全分析

优雅获取方法耗时

背景:需要知道启动阶段所有方法耗时

常规方式

  • 手动埋点
  • long time = System.currentTimeMills();
  • long cost = System.currentTimeMillis() - time;

痛点

  • 侵入性强:这种方式必须要加入自己的代码
  • 不优雅,工作量大:有几百个方法,就要重复几百次
    既然如此,那启动过程耗时监测还用这种方式,那是因为启动过程监测只需要埋两个点,不存在上面说的痛点。所以说没有最优的方案,只有最适合的场景。

AOP: Aspect Oriented Progamming 面向切面编程

  • 针对同一类问题的同一处理
  • 无侵入添加代码
  • 就是把我们某个方面的功能提出来与一批对象进行隔离,这样与一批对象之间降低了耦合性,就可以对某个功能进行编程
    在这里插入图片描述

AspectJ:AspectJ是一个面向切面的框架,它扩展了java语言,所以它有一个专门的编译器用来生成遵守java字节码规范的class文件,它会在编译阶段根据切面点的代码逻辑,在生成的class字节码文件中对应的地方添加相关代码,在运行时实现对切面点的监测。

Join Points:程序运行时的执行点,可以作为切面的地方

  • 函数调用、执行
  • 获取、设置变量
  • 类初始化

PointCut:带条件的JoinPoints

Advice:一种Hook,要插入代码的位置

  • Before:PointCut之前执行
  • After:PointCut 之后执行
  • Around:PointCut之前、之后分别执行

方法耗时监测示例:

自定义获取方法耗时注解

/**
* 监测方法耗时注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
   
    String value();
}

在需要监测的方法处添加注解,针对启动过程中方法耗时监测,可在application的onCreate和LaunchActivity的onCreate和onResume中的方法添加注解,此处以application的onCreate中执行的方法为例

    ...
	/**
     * 初始化极光
     */
    @GetTime("initJiguang")
    private void initJiguang() {
   
        //初始化极光统计
        JAnalyticsInterface.init(getApplicationContext());
        //设置极光统计调试模式
        JAnalyticsInterface.setDebugMode(LogUtil.isDebug);
        //初始化极光IM
        JMessageClient.init(getApplicationContext());
        //注册极光消息回调
        JMessageClient.registerEventReceiver(this);
    }
...
    /**
     * 初始化腾讯Bulgy服务
     */
    @GetTime("initBugly")
    private void initBugly() {
   
        //更多配置参数
        //https://bugly.qq.com/docs/user-guide/instruction-manual-android/
        //CrashReport.initCrashReport(getApplicationContext(), Constant.BUGLY_APP_KEY, LogUtil.isDebug);

        //初始化Bugly所有服务
        //包括异常上报
        //更新
        Bugly.init(getApplicationContext(), Constant.BUGLY_APP_KEY, LogUtil.isDebug);
    }
...

在一个类中统一处理切面点注解,以及添加具体监测的逻辑代码

/**
 * 通过AOP处理添加注解的切面点,实现方法耗时的监测
 */
@Aspect
public class AppContextAop {
   

    @Pointcut("execution(@com.ixuea.courses.mymusic.launch.aop.GetTime * *(..))")
    public void methodAnnotatedWithGetTime(){
   }

    @Around("methodAnnotatedWithGetTime()")
    public void handleJointPoint(ProceedingJoinPoint joinPoint) {
   
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        // 获取注解方法所在的类名
        String className=methodSignature.getDeclaringType().getSimpleName();
        // 获取注解方法的名称
        String methodName=methodSignature.getName();
        // 注解传入的值
        String funName = methodSignature.getMethod().getAnnotation(GetTime.class).value();
        long time = System.currentTimeMillis();

        try {
   
            joinPoint.proceed();
        } catch (Throwable throwable) {
   
            throwable.printStackTrace();
        }
        Log.d("AppContextAop",funName + " cost "+(System.currentTimeMillis() - time));
    }

}

方法耗时日志

<
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值