字节跳动学习笔记:Android性能优化之启动优化实战篇(1)

我们首先需要定义一个统计时间的工具类:

class LaunchRecord {

companion object {

private var sStart: Long = 0

fun startRecord() {
sStart = System.currentTimeMillis()
}

fun endRecord() {
endRecord(“”)
}

fun endRecord(postion: String) {
val cost = System.currentTimeMillis() - sStart
println(“=== p o s t i o n = = = postion=== postion===cost”)
}
}
}

启动时埋点我们直接在Application的attachBaseContext中进行打点。那么启动结束应该在哪里打点呢?这里存在一个误区:网上很多资料建议是在Activity的onWindowFocusChange中进行打点,但是onWindowFocusChange这个回调只是表示首帧开始绘制了,并不能表示用户已经看到页面数据了,我们既然做启动优化,那么就要切切实实的得出用户从点击应用图标到看到页面数据之间的时间差值。所以结束埋点建议是在页面数据展示出来进行埋点。比如页面是个列表那就是第一条数据显示出来,或者其他的任何view的展示。

class MyApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
//开始打点
LaunchRecord.startRecord()
}
}

我们分别监听页面view的绘制完成时间和onWindowFocusChanged回调两个值进行对比。

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

mTextView.viewTreeObserver.addOnDrawListener {
LaunchRecord.endRecord(“onDraw”)
}

}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
LaunchRecord.endRecord(“onWindowFocusChanged”)
}
}

打印的数据为:

=onWindowFocusChanged=322
=onDraw=328

可以很明显看到onDraw所需要的时长是大于onWindowFocusChanged的时间的。因为我们这个只是简单的数据展示没有进行网络相关请求和复杂布局所以差别不大。

这里需要说明下:addOnDrawListener 需要大于API 16才可以使用,如果为了兼顾老版本用户可以使用addOnPre DrawListener来代替。

手动打点方式统计的启动时间比较精确而且可以带到线上使用,推荐这种方式。但在使用的时候要避开一个误区就是启动结束的埋点我们要采用Feed第一条数据展示出来来进行统计。同时addOnDrawListener要求API 16,这两点在使用的时候需要注意的。

优化工具的选择


在做启动优化的时候我们可以借助三方工具来更好的帮助我们理清各个阶段的方法或者线程、CPU的执行耗时等情况。主要介绍以下两个工具,我在这里就简单介绍下,读者朋友们可以线下自己取尝试下。

TraceView:

TraceView是以图形的形式展示执行时间、调用栈等信息,信息比较全面,包含所有线程。

使用:

开始:Debug.startMethodTracing(“name” )
结束:Debug.stopMethodTracing(“” )

最后会生成一个文件在SD卡中,路径为:Andrid/data/packagename/files。

因为traceview收集的信息比较全面,所以会导致运行开销严重,整体APP的运行会变慢,这就有可能会带偏我们优化的方向,因为我们无法区分是不是traceview影响了启动时间。

SysTrace:

Systrace是结合Android内核数据,生成HTML报告,从报告中我们可以看到各个线程的执行时间以及方法耗时和CPU执行时间等。API 18以上使用,推荐使用TraceCompat,因为这是兼容的API。

使用:

开始:TraceCompat.beginSection("tag ")
结束:TraceCompat.endSection()

然后执行脚本:

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app

给大家解释下各个字端的含义:

  • -b 收集数据的大小
  • -t 时间
  • -a 监听的应用包名
  • -o 生成文件的名称

Systrace开销较小,属于轻量级的工具,并且可以直观反映CPU的利用率。这里需要说明下在生成的报告中,当你看某个线程执行耗时时会看到两个字端分别好似walltime和cputime,这两个字端给大家解释下就是walltime是代码执行的时间,cputime是代码真正消耗cpu的执行时间,cputime才是我们优化的重点指标。这点很容易被大家忽视。

优雅获取方法耗时

上文中主要是讲解了如何监听整体的应用启动耗时,那么我们如何识别某个方法所执行的耗时呢?

我们常规的做法和上文中一样也是打点,如:

public class MyApp extends Application {

@Override
public void onCreate() {
super.onCreate();

initFresco();
initBugly();
initWeex();
}

private void initWeex(){
LaunchRecord.Companion.startRecord();
InitConfig config = new InitConfig.Builder().build();
WXSDKEngine.initialize(this, config);
LaunchRecord.Companion.endRecord(“initWeex”);
}

private void initFresco() {
LaunchRecord.Companion.startRecord();
Fresco.initialize(this);
LaunchRecord.Companion.endRecord(“initFresco”);
}

private void initBugly() {
LaunchRecord.Companion.startRecord();
CrashReport.initCrashReport(getApplicationContext(), “注册时申请的APPID”, false);
LaunchRecord.Companion.endRecord(“initBugly”);
}
}

控制台打印:

=initFresco=278
=initBugly=76
=initWeex=83

但是这种方式导致代码不够优雅,并且侵入性强而且工作量大,不利于后期维护和扩展。

下面我给大家介绍另外一种方式就是AOP。AOP是面向切面变成,针对同一类问题的统一处理,无侵入添加代码。

我们主要使用的是AspectJ框架,在使用之前呢给大家简单介绍下相关的API:

  • Join Points 切面的地方:函数调用、执行,获取设置变量,类初始化
  • PointCut:带条件的JoinPoints
  • Advice:Hook 要插入代码的位置。
  • Before:PointCut之前执行
  • After:PointCut之后执行
  • Around:PointCut之前之后分别执行

具体代码如下:

@Aspect
public class AOPJava {

@Around(“call(* com.optimize.performance.MyApp.**(…))”)
public void applicationFun(ProceedingJoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}

Log.d(“AOPJava”, name + " == cost ==" + (System.currentTimeMillis() - time));

}
}

控制台打印结果如下:

MyApp.initFresco() == cost ==288
MyApp.initBugly() == cost ==76
MyApp.initWeex() == cost ==85

但是我们没有在MyApp中做任何改动,所以采用AOP的方式来统计方法耗时更加方便并且代码无侵入性。具体AspectJ的使用学习后续文章来介绍。

异步优化


上文中我们主要是讲解了一些耗时统计的方法策略,下面我们就来具体看下如何进行启动耗时的优化。

在启动分类中我们讲过应用启动任务中有一个空白window,这是可以作为优化的一个小技巧就是Theme的切换,使用一个背景图设置给Activity,当Activity打开后再将主题设置回来,这样会让用户感觉很快。但其实从技术角度讲这种优化并没有效果,只是感官上的快。

首先现在res/drawable中新建lanucher.xml文件:






将其设置给第一个打开的Activity,如MainActivity:







最后在MainActivity中的onCreate的spuer.onCreate()中将其设置会原来的主题:

override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
}

}

这样就完成了Theme主题的切换。

下面我们说下异步优化,异步优化顾名思义就是采用异步的方式进行任务的初始化。新建子线程(线程池)分担主线称任务并发的时间,充分利用CPU。

如果使用线程池那么设置多少个线程合适呢?这里我们参考了AsyncTask源码中的设计,获取可用CPU的数量,并且根据这个数量计算一个合理的数值。

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

@Override
public void onCreate() {
super.onCreate();

ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);

pool.submit(new Runnable() {
@Override
public void run() {
initFresco();
}
});

pool.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});

pool.submit(new Runnable() {
@Override
public void run() {
initWeex();
}
});

}

这样我们就将所有的任务进行异步初始化了。我们看下未异步的时间和异步的对比:

未异步时间:======210
异步的时间:======3

可以看出这个时间差还是比较明显的。这里还有另外一个问题就是,比如异步初始化Fresco,但是在MainActivity一加载就要使用而Fresco是异步加载的有可能这时候还没有加载完成,这样就会抛异常了,怎么办呢?这里教大家一个新的技巧就是使用CountDownLatch,如:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

//1表示要被满足一次countDown
private CountDownLatch mCountDownLatch = new CountDownLatch(1);

@Override
public void onCreate() {
super.onCreate();

ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);

pool.submit(new Runnable() {
@Override
public void run() {
initFresco();
//调用一次countDown
mCountDownLatch.countDown();
}
});

pool.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});

pool.submit(new Runnable() {
@Override
public void run() {
initWeex();
}
});

try {
//如果await之前没有调用countDown那么就会一直阻塞在这里
mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。

image

Android高级架构师之路很漫长,一起共勉吧!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。

[外链图片转存中…(img-dvr8gCrX-1712397754877)]

Android高级架构师之路很漫长,一起共勉吧!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值