android--性能优化1--首屏优化&启动速度与执行效率检测
Posted 六道对穿肠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android--性能优化1--首屏优化&启动速度与执行效率检测相关的知识,希望对你有一定的参考价值。
文章目录
- 我会先把实战篇写在前面, 把理论篇写在后面. 我太清楚实用党是怎么想的了,只想抓紧时间吧APP的性能搞好. 关于原理方面都往后放. 哈哈 . 因为我以前也是这样的. 为了方便快速使用 直接上干货. 先按照教程做,后面有功夫再看理论.
实战篇
卡顿工具 BlockCanary
http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/
traceview 工具使用
操作步骤
利用Debug类的startMethodTracing和stopMethodTracing函数分别来打开和关闭日志记录功能。记录两个方法中间所有函数执行的时间统计.
方法执行完后,会在手机sdcard中生成一个dmtrace.trace文件
- 第一步: 在MyApplication 的onCreate 最前面输入如下代码
if (BuildConfig.DEBUG)
Debug.startMethodTracing("APP");
- 第二步: 在MyApplication 的onCreate 结束位置输入如下代码
if (BuildConfig.DEBUG)
Debug.stopMethodTracing();
- 第三步: 找到生成的文件
在/sdcard/android/data/packageName/files 目录下面 有一个 APP.trace 的文件
从androidstudio 上面打开来看 就有下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Kv6rsBg-1612277341019)(https://raw.githubusercontent.com/liudao01/picture/master/img/QQ%E5%9B%BE%E7%89%8720210130123716.png)]
操作步骤2
使用android studio profiler 双击cpu -> record 按钮 即可有文件
systrace 工具使用
操作步骤
- 第一步: 在MyApplication 的onCreate 最前面输入如下代码
if (BuildConfig.DEBUG)
TraceCompat.beginAsyncSection("App_onCreate",0);
// Debug.startMethodTracing("APP");
- 第二步: 在MyApplication 的onCreate 结束位置输入如下代码
if (BuildConfig.DEBUG)
TraceCompat.endSection();
// Debug.stopMethodTracing();
- 第三步: 进入sdk目录 使用脚本命令
我的在这里
E:\\dev\\sdk\\platform-tools\\systrace>
执行下面的命令
package-name 是app的名字
./systrace.py -b -t 5 sched gfx view wm am app webview -a <package-name>
./systrace.py -b 32768 -t 5 -a <package-name>
systrace.py -b 32768 -t 5 -a com.sinochem.www.car.owne -o performance.html sched gfx view wm am app
遇到问题: ImportError: No module named win32con
下载地址 https://pypi.org/project/pypiwin32/219/#files
PyPI(Python Package Index)是python官方的第三方库的仓库,找python2.7对应的版本。
打开网页即可
AOP 工具 hugo 的使用
为什么用这个: 用于埋点计算函数运行的时间. 非侵入性.
使用方法
- 项目根目录build.gradle添加hugo插件依赖
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
- 主工程或者library的录build.gradle中声明hugo插件。
apply plugin: 'com.jakewharton.hugo'
注意在每个lib里面都要加上 你想在那个lib里面打印就在那个lib里面加上这句
3. 在类或方法上声明@DebugLog注解。
@Override @DebugLog
public void onCreate()
super.onCreate();
效果
2021-01-31 15:03:01.871 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 15:03:02.161 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [289ms]
可以看到运行时间289ms
参考资料:
https://github.com/JakeWharton/hugo
https://juejin.cn/post/6844903945186476040
https://xiaozhuanlan.com/topic/1428095376
BlockCanary 使用
AOP 工具使用 AspectJ
https://ciruy.blog.csdn.net/article/details/102621417?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
https://blog.csdn.net/eclipsexys/article/details/54425414
为什么用这个: 用于埋点计算函数运行的时间. 非侵入性.
操作步骤
Android中使用AspectJ
- 第一步:
在项目的根目录的build.gradle文件中添加依赖,修改后文件如下
repositories
jcenter()
dependencies
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
- 然后在项目或者库的build.gradle文件中添加AspectJ的依赖
compile 'org.aspectj:aspectjrt:1.8.9'
同时在该文件中加入AspectJX模块
apply plugin: 'android-aspectjx'
- aspectjx默认会遍历项目编译后所有的.class文件和依赖的第三方库去查找符合织入条件的切点,为了提升编译效率,可以加入过滤条件指定遍历某些库或者不遍历某些库。
includeJarFilter和excludeJarFilter可以支持groupId过滤,artifactId过滤,或者依赖路径匹配过滤
aspectjx
//织入遍历符合条件的库
includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
//排除包含‘universal-image-loader’的库
excludeJarFilter 'universal-image-loader'
- 创建性能收集类PreformanceAop
代码如下,直接拷贝
package android.androidlib.utils;
import android.util.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
* @author liuml
* @explain
* @time 2021/1/30 20:47
*/
@Aspect
public class PreformanceAop
/**
* 针对调用的接口(日志的形式)观察执行时间
*/
private static final String MyApplication = "call(** com.application.MyApplication.*(..))";
private static final String AcgGlobalInitImpl = "call(** com.init.AcgGlobalInitImpl.*(..))";
private static final String SplashActivity = "call(** com.biz.splash.SplashActivity.*(..))";
/**
* 方法的执行时长
*/
@Around(MyApplication + "||" + AcgGlobalInitImpl + "||" + SplashActivity)
public void getComicsApplicationTime(ProceedingJoinPoint joinPoint)
getTime(joinPoint);
/**
* 获取一个类中所有方法的执行时长
* call : 代表的是在方法调用的地方添加代码
* 第一个 * : 代表的方法的修饰符
* 第二个 * : 代表的是方法的返回值类型
* 第三个 * : 类名
* 第四个 * : 代表的是方法名
* (..) : 参数列表
* 该方法拦截的是: com.example.qydemo1包下的所有类的方法
*/
private void getTime(ProceedingJoinPoint joinPoint)
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try
joinPoint.proceed();
catch (Throwable throwable)
throwable.printStackTrace();
LogUtils.d(name + " 花费 " + (System.currentTimeMillis() - time)+" 毫秒");
// LogUtils.d(name + " cost " + (System.currentTimeMillis() - time));
- 注意 MyApplication = “call(** com.application.MyApplication.*(…))”; 是你想拦截的类,每个人的都不一样我这里只是demo样例
AspectJ 语法
https://juejin.cn/post/6844903941054922760
https://zhuanlan.zhihu.com/p/72575832
https://www.jianshu.com/p/691acc98c0b8
优化方案:
异步线程优化:
在MyApplication 初始化一些第三方库的时候使用线程池异步加载
原来的时间:
2021-01-31 15:03:01.871 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 15:03:02.161 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [289ms]
效果:
2021-01-31 16:46:45.652 26568-26568/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 16:46:45.882 26568-26568/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [229ms]
优化了60多 毫秒.
针对主线程需要异步线程的结果所采用的方案 CountDownLauch:
不仅可以用在这里. 也可以用在其他地方.
比如在第一个页面就需要异步的结果,就可以使用 CountDownLauch
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
使用方法:
- 创建
private CountDownLatch mCounDownLatch = new CountDownLatch(1);
这里的1 是指有几个地方用到了,也就是说异步任务有几个需要主线程确定执行完毕的.
- 使用
ThreadManager.getDownloadPool().execute(new Runnable()
@Override
public void run()
//屏幕适配
configUnits();
mCounDownLatch.countDown();
);
//在代码的最后 也就是需要确定的地方加上这个
try
mCounDownLatch.await();
catch (InterruptedException e)
e.printStackTrace();
这样就形成了 必须等屏幕适配结束 才继续.
使用启动器
为什么采用启动器. 比如多个异步任务,第二个任务需要第一个任务的结果.这样就不好处理了,任务之间有以来关系不好处理. 另一方面不好统计, 代码也不优雅且维护成本高.
启动器核心思想: 充分利用cpu多核,自动梳理任务顺序
- 代码task化,启动逻辑抽象为Task(比如初始化每一个初始化的操作进行抽象,以及分类)
- 根据素有任务以来关系排序生成一个有向无环图.(对所有任务进行排序)
- 多现场按照排序后的优先级依次执行
这个有点像gradle 的task了 思想应该是共通的
启动流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kNtifQG7-1612277341022)(https://raw.githubusercontent.com/liudao01/picture/master/img/20210131172532.png)]
我的APP内应用效果
理论篇
启动时间测量方式
1 ADB命令
adb shell am start -W packegname/首屏activity
- thisTime : 最后一个Activity启动耗时
- TotalTime : 所有Activity 启动耗时
- WaitTime : AMS 启动Activity 的总耗时.
特征: 线下方便. 非严谨, 无法带到线上.
启动时间测量方式
2 手动打点 使用LaunchTimer工具类记录时间
- 抽取一个类LaunchTimer 用作工具打点的类
- 代码如下: 拷贝拿走
public class LaunchTimer
private static long sTime;
public static void startRecord()
sTime = System.currentTimeMillis();
public static void endRecord()
endRecord("");
public static void endRecord(String msg)
long cost = System.currentTimeMillis() - sTime;
LogUtils.d(msg + " cost " + cost + "ms");
- 首页若有列表,在列表adapter里面打点
boolean mHasRecorde;
if (helper.getAdapterPosition() == 0 && !mHasRecorde)
mHasRecorde = true;
helper.getView(R.id.item_container).getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
@Override
public boolean onPreDraw()
helper.getView(R.id.item_container).getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTimer.endRecord("feed show");
return true;
);
-
在MainActivity里面的 onWindowFocesChanged 里面打点
-
代码如下
@Override
public void onWindowFocusChanged(boolean hasFocus)
super.onWindowFocusChanged(hasFocus);
LaunchTimer.endRecord("onWindowFocusChanged");
- 打印效果如下:
feed show cost 1611909626616ms
onWindowFocusChanged cost 1611909627011ms
- 手动打点误区: onWindowFocusChanged 只是首帧时间 并不是用户所看到的时间
- 正解: 真实数据展示, (首页若有列表)feed第一条展示的时候,在adapter里面加上监听/ 页面展示的时候
所以我们认为应当将feed第一条展示的时候,当做启动开始时间,在onWindowFocusChanged 是当做启动时间是偏早的.
traceview 工具使用 使用说明
startMethodTracing 有几个重载的方法.
* @param tracePath Path to the trace log file to create. If @code null,
* this will default to "dmtrace.trace". If the file already
* exists, it will be truncated. If the path given does not end
* in ".trace", it will be appended for you.
* @param bufferSize The maximum amount of trace data we gather. If not
* given, it defaults to 8MB.
*/
public static void startMethodTracing(String tracePath, int bufferSize)
startMethodTracing(tracePath, bufferSize, 0);
下面是解析
/**
* tracePath 是路径 bufferSize 文件最大的大小 默认8M 0是个flag 只有一个选项
*/
startMethodTracing(tracePath, bufferSize, 0);
下表是Traceview界面每个字段表示的含义
列名 | 描述 |
---|---|
Name | 该线程运行过程中所调用的函数名 |
Incl Cpu Time | 某函数占用的CPU时间,包含内部调用其它函数的CPU时间 |
Excl Cpu Time | 某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间 |
Incl Real Time | 某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间 |
Excl Real Time | 某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间 |
Call+Recur Calls/Total | 某函数被调用次数以及递归调用占总调用次数的百分比 |
Cpu Time/Call | 某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间 |
Real Time/Call | 同CPU Time/Call类似,只不过统计单位换成了真实时间 |
- 图表如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J3qxR0Li-1612277341026)(https://raw.githubusercontent.com/liudao01/picture/master/img/QQ%E5%9B%BE%E7%89%8720210130123716.png)]
图表说明:
Thread 说明: 看图可知有12个线程.
左边的图表作用很大:
- 系统API 是橙色
- 应用自身的调用是绿色
- 第三方的 sdk是蓝色
flam chart
重要性不高
火焰图 只用来收集相同函数调用的次数
topDown
告诉我们函数的调用列表, 右键点击 jump to source 可以跳转到源码
buttomUp
重要性不高
用于显示一个函数的调用列表, 和topDown 是相反的
使用traceview注意事项
- 运行时开销很大 程序一定会变慢. 可能会带骗优化方向
- 新版本是用cpu profiler
知道哪个函数耗时,就可以逐个优化耗时操作了
systrace 工具使用 使用说明
-
结合Android内核的数据,生成html报告
-
API18以上使用,推荐TraeCompat
-
systrace 实际上是个python脚本.
-
脚本解析
systrace.py -b 32768 -t 5 -a com.sinochem.www.car.owne -o performance.html sched gfx view wm am app
-b 是生成最大文件的大小 , -t 是监听事件 , -a 后面跟的是包名 , -o 后面是生成的文件名.
以上是关于android--性能优化1--首屏优化&启动速度与执行效率检测的主要内容,如果未能解决你的问题,请参考以下文章