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

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();
}

}

这样就会一直阻塞在await这里,直到Fresco初始化完成。

以上这种方式大家觉得如何呢?可以解决异步问题,但是我的Demo中只有三个需要初始化的任务,在我们真实的项目中可不止,所以在项目中我们需要书写很多的子线程代码,这样显然是不够优雅的。部分代码需要在初始化的时候就要完成,虽然可以使用countDowmLatch,但是任务较多的话,也是比较麻烦的,另外就是如果任务之间存在依赖关系,这种使用异步就很难处理了。

针对上面这些问题,我给大家介绍一种新的异步方式就是启动器。核心思想就是充分利用CPU多核,自动梳理任务顺序。核心流程:

  • 任务代码Task化,启动逻辑抽象为Task
  • 根据所有任务依赖关系排序生成一个有向无环图
  • 多线程按照排序后的优先级依次执行

TaskDispatcher.init(PerformanceApp.)TaskDispatcher dispatcher = TaskDispatcher.createInstance()dispatcher.addTask(InitWeexTask())
.addTask(InitBuglyTask())
.addTask(InitFrescoTask())
.start()dispatcher.await()LaunchTimer.endRecord()

最后代码会变成这样,具体的实现有向无环图逻辑因为代码量很多,不方便贴出来,大家可以关注公众号获取。

使用有向无环图可以很好的梳理出每个任务的执行逻辑,以及它们之间的依赖关系

延迟初始化


关于延迟初始化方案这里介绍两者方式,一种是比较常规的做法,另外一个是利用IdleHandler来实现。

常规做法就是在Feed显示完第一条数据后进行异步任务的初始化。比如:

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

mTextView.viewTreeObserver.addOnDrawListener {
// initTask()
}

}

这里有个问题就是更新UI是在Main线程执行的,所以做初始化任务等耗时操作时会发生UI的卡顿,这时我们可以使用Handler.postDelay(),但是delay多久呢?这个时间是不好控制的。所以这种常规的延迟初始化方案有可能会导致页面的卡顿,并且延迟加载的时机不好控制。

IdleHandler方式就是利用其特性,只有CPU空闲的时候才会执行相关任务,并且我们可以分批进行任务初始化,可以有效缓解界面的卡顿。代码如下:

public class DelayInitDispatcher {

private Queue mDelayTasks = new LinkedList<>();

private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if (mDelayTasks.size() > 0) {
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

img

img

img

img

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

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

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

最后

分享一份工作1到5年以上的Android程序员架构进阶学习路线体系,希望能对那些还在从事Android开发却还不知道如何去提升自己的,还处于迷茫的朋友!

  • 阿里P7级Android架构师技术脑图;查漏补缺,体系化深入学习提升

  • **全套体系化高级架构视频;**七大主流技术模块,视频+源码+笔记

有任何问题,欢迎广大网友一起来交流

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

最后

分享一份工作1到5年以上的Android程序员架构进阶学习路线体系,希望能对那些还在从事Android开发却还不知道如何去提升自己的,还处于迷茫的朋友!

  • 阿里P7级Android架构师技术脑图;查漏补缺,体系化深入学习提升

    [外链图片转存中…(img-hbBj9uhg-1713584296981)]

  • **全套体系化高级架构视频;**七大主流技术模块,视频+源码+笔记

[外链图片转存中…(img-HMfXiT2b-1713584296982)]

有任何问题,欢迎广大网友一起来交流

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值