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文件

  1. 第一步: 在MyApplication 的onCreate 最前面输入如下代码
 if (BuildConfig.DEBUG) 
            Debug.startMethodTracing("APP");
        
  1. 第二步: 在MyApplication 的onCreate 结束位置输入如下代码
 if (BuildConfig.DEBUG) 
            Debug.stopMethodTracing();
    
  1. 第三步: 找到生成的文件

在/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 工具使用

操作步骤

  1. 第一步: 在MyApplication 的onCreate 最前面输入如下代码
        if (BuildConfig.DEBUG) 
            TraceCompat.beginAsyncSection("App_onCreate",0);
//            Debug.startMethodTracing("APP");
        
  1. 第二步: 在MyApplication 的onCreate 结束位置输入如下代码
        if (BuildConfig.DEBUG) 
            TraceCompat.endSection();
//            Debug.stopMethodTracing();
        
  1. 第三步: 进入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 的使用

为什么用这个: 用于埋点计算函数运行的时间. 非侵入性.

使用方法

  1. 项目根目录build.gradle添加hugo插件依赖
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
  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

  1. 第一步:
    在项目的根目录的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

  1. 然后在项目或者库的build.gradle文件中添加AspectJ的依赖
compile 'org.aspectj:aspectjrt:1.8.9'

同时在该文件中加入AspectJX模块

apply plugin: 'android-aspectjx'
  1. aspectjx默认会遍历项目编译后所有的.class文件和依赖的第三方库去查找符合织入条件的切点,为了提升编译效率,可以加入过滤条件指定遍历某些库或者不遍历某些库。

includeJarFilter和excludeJarFilter可以支持groupId过滤,artifactId过滤,或者依赖路径匹配过滤

aspectjx 
    //织入遍历符合条件的库
    includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
    //排除包含‘universal-image-loader’的库
    excludeJarFilter 'universal-image-loader'

  1. 创建性能收集类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时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

使用方法:

  1. 创建
 private CountDownLatch mCounDownLatch = new CountDownLatch(1);

这里的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--首屏优化&启动速度与执行效率检测的主要内容,如果未能解决你的问题,请参考以下文章

高性能网站搭建-前端性能优化 (附Vue首屏加载时间优化详细方案)

前端性能优化指南(含移动端)

提升首屏加载速度:vue-cli的首屏性能优化

CSS性能优化的技巧

移动H5前端性能优化指南

10 秒 到 2秒 超简单的Vue项目首屏优化实践