文章目录
黑白屏优化
黑白屏原因
在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:
- 自定义继承自AppTheme的主题
- 将启动Activity的theme设置为自定义主题
- 在启动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));
}
}
方法耗时日志
<