Android性能分析与优化学习(二) App启动优化

一、App启动优化介绍
1、背景介绍
  • 第一体验
  • 八秒定律
2、启动分类
  • 冷启动
  • 耗时最多,衡量标准
    ClickEvent -> IPC -> Process.start ->ActivityThread(单独app进程入口类) ->bindApplication(通过反射创建Application以及调用与Application相关的生命周期) ->LifeCycle(Activity生命周期) -> ViewRootImpl(开始真正的界面绘制)
  • 热启动,最快

后台 -> 前台

  • 温启动,较快

LifeCycle(只会重走activity生命周期,不会重新进程创建)
#####3、相关任务

  • 冷启动之前:启动App,加载空白Window,创建进程
  • 随后任务:创建Application,启动主线程,创建MainActivity,加载布局,布置屏幕,首帧绘制
4、优化方向
  • Application和Activity生命周期
二、启动时间测量方式
1、adc命令

adb shell am start -W packagename/首屏Activity
命令

  • ThisTime:最后一个Activity启动耗时
  • TotalTime:所有Activity启动耗时
  • WaitTime:AMS启动Activity的总耗时
    问题:线下使用方便,不能带到线上,非严谨精确时间
2、手动打点

启动时埋点,启动结束时埋点,差值

public class LaunchTimer {
    private static long sTime;

    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }

    public static void endRecord() {
        long cost = System.currentTimeMillis() - sTime;
        Log.i("cost", cost + "");
    }
}

这个回调是应用程序能接收到的最早的回调,在这里开始计时

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        LaunchTimer.startRecord();
        MultiDex.install(this);
    }

误区:onWindowFocusChanged只是首帧时间,并不能代表界面已经展示出来了,要在真实用户展示时间埋点,比如在列表第一条数据展示时候

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && !mHasRecorded) {
            mHasRecorded = true;
            holder.itemView.getViewTreeObserver()
                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
                    LaunchTimer.endRecord();
                    return false;
                }
            });
        }
    }
三、 启动优化工具选择
1、traceview
  • 图形化界面形式展示代码执行时间,调用栈等
  • 信息全面,包含所有线程
  • 使用方式
//在开始时调用
Debug.startMethodTracing("文件名");
//结束时调用
Debug.stopMethodTracing();

生成的文件存放在sd卡:Android/data/packagename/files,运行app后开路径下找到文件生成的文件双击打开文件生成的文件
最上面的是时间范围,可移动鼠标拖动
中间的THREADS是线程,可以看到有17个线程,选中一个线程就可以看到下面线程做的事情,当前看的是mian线程

  • Call Chart
    每一行都是一个方法的调用时间消耗,他的垂直方向是被调用者
    系统api是橙色,应用自身的方法是绿色,第三方api是蓝色(包括java语言的api)
  • TopDownTopDown可以看到方法执行的总时间Total,Self时间和Children时间
    可以选择看Wall Colock Time (线程真正执行时间),Thread Time (CPU执行时间)
  • Flame Chart
    收集相同的调用顺序Flame Chart
  • Bottom Up
    一个函数的调用列表,谁调用了我Bottom Up
  • 问题:加入了traceview,运行开销严重,整体开销都会变慢,可能会带偏优化方向
2、systrace
  • 结合Android内核的数据,生成Html报告
  • 使用方式
python systrace.py t 10 [other-options] [categories]
//开始时
TraceCompat.beginSection("systrace");
//结束时
TraceCompat.endSection();

到systrace目录下运行脚本,我的是E:\SDK\platform-tools\systrace
注意python要2.7版本的
python systrace.py --time=10 -o mytrace.html sched gfx view wm
也可以用Android Device Monitor来生成
image.png
打开生成的html
image.png
该报告列出了呈现UI帧并指示沿时间线的每个渲染帧的每个进程。用绿色框架圆圈表示在16.6毫秒内渲染以保持每秒60帧稳定所需的帧。渲染时间超过16.6毫秒的帧用黄色或红色框架圆圈表示。
可以选中一个线程查看他的时间消耗
image.png

  • 官方文档:https://source.android.com/devices/tech/debug/systrace
  • 优点:轻量级,开销小,直观反馈CPU利用率
  • cputime和walltime的区别:cputime是代码消耗cpu的时间(重点指标),walltime是代码执行时间。
    举例:锁冲突,可能某个线程在等待锁,导致walltime时间看起来很长,但是对CPU没有占用
3、优雅获取方法耗时

1.常规方式:
背景:需要知道启动阶段所有方法耗时
实现:手动埋点
入侵性强,工作量大
2.AOP
Aspect Oriented Programming,面向切面编程,针对同一类问题的统一处理,无侵入添加代码
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
使用AspectJ,在根build.gradle下配置:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}

app项目的build.gradle及新建的module的build.gradle里都应用插件

apply plugin: 'android-aspectjx'
dependencies {
    implementation 'org.aspectj:aspectjrt:1.8.9'
}
  • Join Points
    程序运行时的执行点,可以作为切面的地方。(函数调用、执行,获取、设置变量,类的初始化)
  • PointCut
    带条件的JoinPoints
  • Advice
    一种Hook,要插入代码的位置。before:PointCut之前执行。after:PointCut之后执行。around:之前之后分别执行
  • 语法介绍
@Before("execution(*android.app.Activity.on**(...))
public void onActivityCalled(JoinPoint joinPoint) throws Throwable{
    ...
}

Before:Advice,具体插入位置
execution:处理Join Point的类型,call(插入在函数体里面)、execution(插入在函数体外面)
(android.app.Activity.on*(…)):匹配规则
onActivityCalled:要插入的代码
使用AOP实现启动时间监听

@Aspect
public class PerformanceAop {
    //Around在每个方法执行之前和之后分别插入代码,只切了BaseApplication里面的函数**(..)任何方法参数
    @Around("call(* com.test.common.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name=signature.getName();
        long time = System.currentTimeMillis();
        Log.i("cost", System.currentTimeMillis() - time + "");
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i("cost", name + (System.currentTimeMillis() - time));
    }
}

有点:无侵入性,修改方便

四、异步优化
1、优化小技巧
  • Theme切换:感觉上的快,先进入一个闪屏页
    image.png先使用这个theme,在MainActivity的super.onCreate()之前切换回来
2、异步优化
  • 核心思想:子线程分担主线程任务,并行减少时间
    image.png
    异步优化注意
  • 不符合异步要求的,一种是修改代码使其满足,一种是放弃异步的优化
  • 需要在某个阶段完成,可以使用CountDownLatch
  • 区分CPU密集型和IO密集型任务
public class AppApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //线程池数量,可以参照AsyncTask,
        int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        // We want at least 2 threads and at most 4 threads in the core pool,
        // preferring to have 1 less than the CPU count to avoid saturating
        // the CPU with background work
        int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
        int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        ExecutorService service=Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                initMap();
            }
        });
        ......
    }
}

当然并不是所有代码都可以满足异步优化,比如在子线程中

Handler handler = new Handler();

就会崩溃,因为在子线程中他找不到looper,解决方案

Handler handler = new Handler(Looper.getMainLooper());

但是项目中,总是会有一些代码必须要在主线程中执行,这种情况就要放弃异步优化。
比如初始化的代码必须在application的onCreate()中结束,在activity中调用不到就会崩溃,针对这种情况可以使用CountDownLatch

    //条件被满足1次
    private CountDownLatch countDownLatch=new CountDownLatch(1);
    @Override
    public void onCreate() {
        super.onCreate();
        service.submit(new Runnable() {
            @Override
            public void run() {
                initMap();
                //条件被满足
                countDownLatch.countDown();
            }
        });

        //如果条件没满足就等待
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //条件被满足了,onCreate()才会执行结束
    }

以上常规的异步方案,是有一些问题的:代码不够优雅维护成本高,有些操作有依赖关系,需要有先后顺序执行的,不方便统计

3、异步优化升级

启动器介绍
核心思想:充分利用CPU多核,自动梳理任务顺序
启动器流程
-代码Task化,启动逻辑抽象为Task

  • 根据所有任务依赖关系排序生成一个有向无环图
  • 多线程按照排序后的优先级依次执行
    启动器流程图
4、更优秀的延迟初始化方案

常规方案

  • new Handler().postDelayed
  • 痛点:时机不受控制,可能导致卡顿
    更优方案
  • 核心思想:对延迟任务进行分批初始化
    利用IdleHandler特性空闲执行
public class DelayInitDispatcher {

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

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}
5、优化总方针
  • 异步,延迟,懒加载
  • 技术、、业务相结合
    注意事项
  • wall time和cpu time
    cpu time才是优化方向,按照systrace及cpu time跑满cpu
  • 监控的完善:线上监控多阶段时间(App,Application,生命周期间隔时间),处理聚合看趋势
  • 收敛启动代码修改权限,结合ci修改启动代码需要Review或通知
    其他方案
  • 提前加载SharePreference
    如果不加限制,可能有几十个类在使用几十个文件,调用get方法会异步加载配置文件,加载到内存当中,在get或者put一个属性时候如果load到内存中的操作没有执行完,就会阻塞进行等待
    Multidex之前加载,利用此阶段的CPU
    覆写getApplicationContext()返回this
  • 启动阶段不启动子进程
    很多App会有多个进程,子进程会影响主进程的启动时间,因为子进程会共享CPU资源,导致主进程CPU紧张。
    注意启动顺序:App onCreate之前是ContentProvider,
  • 类加载优化:提前异步类加载
    每只用一个类都是通过classloader,如果启动太多类,会延迟启动时间。
    通过Class.forName()只加载类本身及其静态变量的引用类,如果new类实例可以额外加载类成员变量的引用类,主要需要根据业务情况判断。
    哪些类需要提前异步类加载,可以通过替换系统的ClassLoader,在自定义的ClassLoader打印log
  • 启动阶段抑制GC
  • CPU锁频
6、模拟问题
  • 你启动优化怎么做的?要讲整个过程
    分析现状,确认问题:比如,在某个版本发现启动速度变得特别卡顿,用户反馈变多,所以进行优化,对启动代码进行梳理,发现启动流程非常复杂了,通过一系列工具来确认出来在主线程中执行了太多代码。
    针对性优化:比如进行了异步初始化,比如有些代码优先级不是很高,可以延迟执行
    长期保持优化效果:
  • 是怎么异步的,异步过程中遇到了哪些问题
    体现演进过程:最初采用普通异步方案,之后发现不方便,寻找新的解决方案,介绍启动器
  • 做了启动优化,觉得有哪些容易忽略的点
    cpu time,wall time
    注意延迟初始化是优化
    介绍下黑科技,比如类加载,cup拉高频率
  • 版本迭代导致启动变慢有什么好的解决方案
    启动器,结合CI,监控完善
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值