深入启动优化

前言

启动过程是面向用户的第一体验 如果启动过慢或者崩溃 那么卸载概率就会大大增加 
有一个八秒定律是说 如果应用在8秒内没有启动完成 那么大概率会被卸载
所以启动优化还是非常有必要的步骤

老规矩(Show me the code)

Talk is cheap

我们做启动优化的过程主要要思考三个问题:

  • 如何测量启动时间
  • 有哪些方法可以降低启动时间
  • 线上监测如何实现

如何测量启动时间?

测量启动时间有很多种 最简单的一种就是通过打日志实现

1.日志类实现

public class LaunchTimer {
    private static long startTime;

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

    public static void endRecord() {
        endRecord("");
    }

    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - startTime;
        Log.i("main1", msg + " cost time : " + cost);
    }

}

我们可以通过打日志的方式获取启动时间

优点是可以带入线上使用,但是也有几个缺点:

  • 代码侵入太强 不够优雅
  • 可维护性变差

2.AOP方式实现

我们可以通过AOP方式实现无侵入式的日志打印

@Aspect
public class PerformanceAop {

    // 第一个*表示任意返回值
    @Around("call(* com.dsg.androidperformance.PerformanceApp.**(..))")
    public 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.i(name + " cost " + (System.currentTimeMillis() - time));
    }
}

3.使用traceView

在需要监测的代码前后分别加入Debug.startMethodTracing("App");Debug.stopMethodTracing(); 然后通过Profile可以查看TraceView

WechatIMG8.png

可以通过火焰图或者Top Down等等查看耗时

当然也可以使用AS提供的Profile工具 差不多

缺点:
traceView使用开销比较严重 可能会待偏优化方向

4.adb命令方式

adb shell am start -W 包名/包名.MainActivity

可以通过adb获取启动到指定Activity的耗时

5.systrace

个人比较喜欢用这种方式 轻量级 开销小 显示信息也非常的全
通过命令

通过python systrace.py --list-categories查看手机支持的systrace类型

  1. TraceCompat.beginSection("ApponCreate")
  2. TraceCompat.endSection()
  3. 运行python脚本
python systrace.py -t 5 -o ~/Downloads/mytrace.html -a com.dsg.androidperformance sched gfx view wm am res sync

systrace.py 存放在sdk下Platform-tools/systrace下

-o 为指定目录

gfx view wm am res sync 为指定tag

cputime和walltime区别

  1. cputime是代码消耗cpu的时间(重点指标)
  2. walltime是代码执行时间
  3. cputime为什么和walltime不一样,举例:锁冲突

如何启动优化?

主要思路是异步优化和延迟优化 主要优化方向是CPU Time

异步优化

1.线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
threadPoolExecutor.execute(initAMap());
threadPoolExecutor.execute(initBugly());
threadPoolExecutor.execute(initGetDeviceId());
threadPoolExecutor.execute(initJPush());
threadPoolExecutor.execute(initUmeng());
try {
   countDownLatch.await();
} catch (InterruptedException e) {
   e.printStackTrace();
}

通过线程池的方式有几个缺点:

  1. 代码不够优雅 可维护性比较差
  2. 如果各任务之间有依赖性 比如B依赖A 那么没办法实现
  3. 如果A需要在App onCreate生命周期内完成 需要使用countDownLatch来实现 不够优雅 每次改变需要计算 可维护性比较差
2.启动器

为什么我们要设计启动器?

主要就是因为上面几点原因 不可维护 依赖性没法解决

所以实现启动器的时候 我们主要考虑几个点

  1. 使用算法排序 解决依赖性
  2. 需要在某阶段完成
  3. 区分CPU密集型和IO密集型

启动器流程

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

抽象Task设计

    public interface ITask {
    /**
     * 优先级的范围,可根据Task重要程度及工作量指定;之后根据实际情况决定是否有必要放更大
     *
     * @return
     */
    @IntRange(from = Process.THREAD_PRIORITY_FOREGROUND, to = Process.THREAD_PRIORITY_LOWEST)
    int priority();

    void run();

    /**
     * Task执行所在的线程池,可指定,一般默认
     *
     * @return
     */
    Executor runOn();

    /**
     * 依赖关系
     *
     * @return
     */
    List<Class<? extends Task>> dependsOn();

    /**
     * 异步线程执行的Task是否需要在被调用await的时候等待,默认不需要
     *
     * @return
     */
    boolean needWait();

    /**
     * 是否在主线程执行
     *
     * @return
     */
    boolean runOnMainThread();

    /**
     * 只是在主进程执行
     *
     * @return
     */
    boolean onlyInMainProcess();

    /**
     * Task主任务执行完成之后需要执行的任务
     *
     * @return
     */
    Runnable getTailRunnable();

    void setTaskCallBack(TaskCallBack callBack);

    boolean needCall();
}

启动器UML图
WechatIMG9.png
 TaskDispatcher dispatcher = TaskDispatcher.createInstance();

        dispatcher.addTask(new InitAMapTask())
                .addTask(new InitStethoTask())
                .addTask(new InitWeexTask())
                .addTask(new InitBuglyTask())
                .addTask(new InitFrescoTask())
                .addTask(new InitJPushTask())
                .addTask(new InitUmengTask())
                .addTask(new GetDeviceIdTask())
                .start();

        dispatcher.await();

具体启动器代码请看源码 没啥逻辑

延迟加载

我们常规的延迟加载 可能使用Handle+postDelay实现

缺点:

  1. 代码不够优雅 可维护性变差
  2. 可能会阻塞主线程 时机不方便控制
优化方案

使用IdleHandler的特性 在空闲时执行延迟任务

public class DelayTaskDispatcher {
    private LinkedList<Task> delayTasks = new LinkedList<>();

    //空闲时间启动
    private MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if (!delayTasks.isEmpty()) {
                Task task = delayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !delayTasks.isEmpty();
        }
    };

    public DelayTaskDispatcher addTask(Task task) {
        delayTasks.add(task);
        return this;
    }

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

线上如何检测启动时间?

  • 可以自建APM
  • 使用听云等第三方平台
  • Android Vitals

还有哪些优化点?

  • IO优化
    • 建议不出现网络IO
    • 了解启动过程中的所有IO操作
  • 数据重排(减少真正磁盘IO)
  • 类重排(可以通过ReDex实现类重排 提高加载速度)
  • 资源文件重排
  • 厂商通道(HardCoder,Hyper Boost等等)

其他黑科技

  1. 我们可以在启动页先换成一个有背景图的theme,然后启动完成之后oncreate 换成真正的theme
    虽然对启动速度没有提高 但是用户体验提高很多,但是如果在低机型机器上 反而会使更加卡顿 所以建议在Android7.0机器以上开启 优化跟手体验
  2. 尽量不要在启动过程创建子进程
  3. 提前初始化SharePrefence
  4. 类加载优化:提前异步类加载
  5. 启动阶段抑制GC
  6. CPU锁频
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268