美文网首页
启动优化

启动优化

作者: Thisislife | 来源:发表于2019-12-17 22:44 被阅读0次

    一、为什么要做启动优化?

    • 1、app的启动速度是用户的第一体验,影响用户的第一印象。
    • 2、八秒定律:在网页中如果一个网页八秒钟还没有打开,70%的用户都会选择放弃等待;这个对用户的留存影响是很高的,app也是一样的道理。

    二、目录

    我们会通过以下几点来优化启动速度。


    目录.png

    三、替换闪屏页主题

    在冷启动开始时,系统有三个任务。这三个任务是:

    1. 加载并启动应用。
    2. 在启动后立即显示应用的空白启动窗口。
    3. 创建应用进程

    我们看到第二个任务会显示应用的空白启动窗口,导致应用刚启动的时候是白屏(或者黑屏,这个看app的主题),会有很不好的用户体验,我们可以将主题替换成app logo图片避免黑白屏,进入Activity的时候再替换回正常主题。
    这里并没有改变启动速度,只是让用户感觉我们的app已经启动了。

    • 先定义一个drawable文件存放logo,注意这里只能用drawable目录下的文件
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
        <!-- The background color, preferably the same as your normal theme -->
        <item android:drawable="@android:color/white"/>
        <!-- Your product logo - 144dp color version of your app icon -->
        <item>
            <bitmap
                android:src="@drawable/app_logo"
                android:gravity="center"/>
        </item>
    </layer-list> 
    
    • style文件中定义启动theme
    <style name="AppTheme.Launcher" parent="Theme.AppCompat.NoActionBar">
            <item name="android:windowBackground">@drawable/bg_app_launcher</item>
    </style>
    
    • 替换清单文件中启动activity的theme
    <application
            android:name=".MyApplication"
            ......
            android:theme="@style/AppTheme">
    
            <activity android:name=".MainActivity"
                android:theme="@style/AppTheme.Launcher">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    </application>
    
    • 在启动activity被创建后还原主题
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            setTheme(R.style.AppTheme);
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

    四、启动时间测量

    1、adb命令

    adb shell am start -W 包名/全类名

    D:\Android\MyDemo\StartUpTimeDemo>adb shell am start -W com.example.startuptimedemo/com.example.startuptimedemo.MainActivity
    Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.startuptimedemo/.MainActivity }
    Status: ok
    Activity: com.example.startuptimedemo/.MainActivity
    ThisTime: 2436
    TotalTime: 2436
    WaitTime: 2461
    Complete
    

    可以看到启动后打印出三个时间,这里分别介绍下三个值的含义

    • ThisTime: 最后一个Activity启动耗时
    • TotalTime: 所有Activity启动耗时
    • WaitTime: AMS启动总耗时
    启动耗时.png
    demo中只有一个Activity启动,所以ThisTimeTotalTime耗时是一样的。
    • 缺点
      1、只适用于debug模式,不能带到线上;
      2、时间不够精确。

    2、手动打点

    启动时埋点,启动结束埋点,二者差值就是启动时长。

    • 启动开始埋点:一般把Application的attachBaseContext回调方法作为启动开始,这是启动过程中我们能操作的最早的时间点。
    • 启动结束埋点:一般把应用主页面Activity的onWindowFocusChanged回调作为启动结束点,因为onWindowFocusChanged是Activity首桢绘制时间;其实这个也不是很准确,因为他忽略了绘制开始到绘制结束的时间,所以把真实数据的第一条展示甚至屏幕上最后一条数据展示时间作为启动结束更为准确。
    // view 是我们选定作为启动结束的绘制点的View
    // 这里没用addOnDrawListener是因为它兼容最低API 16
    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
         @Override
         public boolean onPreDraw() {
               view.getViewTreeObserver().removeOnPreDrawListener(this);
               // TODO 统计时间
               return true;
         }
     });
    
    • 优点:
      时间更精确,可以带到线上,上传到服务器进行统计分析。

    3、启动优化工具

    traceview

    1、图形的形式展示执行时间、调用栈等
    2、信息全面,包含所有线程

    使用起来也很简单

    // 起点,传入文件名,默认文件路径:sdcard/Android/data/包名/file/文件名.trace
    Debug.startMethodTracing("test");
    // 终点
    Debug.stopMethodTracing();
    

    运行app后打开Android studio右下角 Device File Explorer 工具找到trace文件,打开。

    image.png image.png

    这里就不详细赘述所有功能点了,上图的第5点要注意一下,耗时方式有两种:

    • Wall Clock Time:该线程消耗总时间。
    • Thread Time:CPU在这个线程执行的时间。

    缺点

    • 运行时开销严重,程序变慢。
    • 方法耗时比实际运行增加,容易带偏优化方向。

    systrace

    1、结合Android内核数据,生成html报告
    2、API 18以上使用,推荐TraceCompat

    使用方式

    // 启动
    TraceCompat.beginSection("ApplicationOnCreate");
    // 结束
    TraceCompat.endSection();
    

    运行程序后将命令行切至Android SDK目录,如:F:\Android\sdk\platform-tools\systrace

    执行python命令:
    python systrace.py -b 32768 -t 10 -a 包名 -o test.html sched gfx view wm am app
    说明:

    • -b:buffer 生成文件的限制大小
    • -t:time 选取的时间长度
    • -a:包名
    • -o:生成的文件名

    更多配置可以参考官方文档
    https://developer.android.com/studio/command-line/systrace#command_options

    (记得执行命令前先安装配置Python环境,我在安装的时候也踩了一些坑,老是报错缺少module)

    image.png

    接着生成了test.html文件,打开之


    systrace.png

    可以大概浏览抓取时间段内的CPU执行情况,往下滑:(w键放大、s键缩小)


    image.png

    可以具体查看方法的调用顺序和消耗时长,选中具体的方法查看详细信息,这里要注意Wall DurationCPU Duration的区别

    优点
    • 1、轻量级、开销小
    • 2、直观反馈cpu利用率

    4、获取方法耗时

    手动埋点

    long start = System.currentTimeMillis();
    long cost = System.currentTimeMillis() - start;
    // 获取CPU执行时间 
    SystemClock.currentThreadTimeMillis();
    

    缺点

    • 1、侵入性强
    • 2、工作量大

    AOP方式

    AOP:Aspect Oriented Programing,面向切面编程。可以监控目标方法执行,并做一些操作,这里我们用它来测量方法执行时间。
    使用AOP需要用到AspectJ,项目引入:

    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
    implementation 'org.aspectj:aspectjrt:1.8.+'
    apply plugin: 'android-aspectjx'
    

    使用:
    自定义一个类,定义一个方法来获取目标类中方法执行时间

    @Aspect
    public class AOP {
        // 目标类下的方法执行都会经过这个方法
        @Around("call(* com.example.startuptimedemo.MyApplication.**(..))")
        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();
            }
            Log.i("AOP", name + " cost " + (System.currentTimeMillis() - time));
        }
    }
    
    日志.png
    优点
    • 无侵入性
    • 修改方便

    五、启动任务优化

    1、异步优化

    开发中都会遇到这种情况,在Application的onCreate方法中初始化一些第三方SDK,或者做一些初始化工作,往往这些初始化方法耗时比较多,优化的第一步就是将它们放在子线程中异步执行,减少主线程的耗时。

    public class MyApplication extends Application {
    
        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 executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
    
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    initDelay1();
                }
            });
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    initDelay2();
                }
            });
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    initDelay3();
                }
            });
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    initDelay4();
                }
            });
        }
    

    这样运行起来主线程耗时肯定会少很多,但是同时带来几个问题

    • 所有的初始化工作都能在子线程中执行吗?
    • 如果第三方SDK我们需要用的时候还没初始化结束怎么办?
    • 如果初始化工作之间有依赖怎么办?

    不妨做个假设:
    1、方法initDelay1需要在主线程中执行;
    2、方法initDelay2在app闪屏页就要使用;
    3、方法initDelay4依赖initDelay3
    那我们现在可以这样解决:

    public class MyApplication extends Application {
    
        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));
        // 初始化CountDownLatch 值为1
        private CountDownLatch mCountDownLatch = new CountDownLatch(1);
    
        @Override
        public void onCreate() {
            super.onCreate();
            ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
            // initDelay1在主线程中执行
            initDelay1();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    // initDelay2要在onCreate中初始化完毕
                    initDelay2();
                    // CountDownLatch 值 -1
                    mCountDownLatch.countDown();
                }
            });
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    // initDelay4依赖initDelay3()
                    initDelay3();
                    initDelay4();
                }
            });  
            try {
                // CountDownLatch阻塞住 直到值为 0时才继续执行
                mCountDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    经过一番改造,好像是可以解决上面3个问题,但还是有一些缺陷

    • 代码不优雅
    • 场景不好处理(依赖关系)
    • 维护成本高

    为了解决这些问题,介绍下启动器。

    启动器的概念

    1、充分利用CPU多核。
    2、代码task化,启动逻辑抽象成Task。
    3、根据所有任务依赖关系排序生成一个有向无环图。
    4、多线程按照排序后的优先级依次执行。

    流程图.png

    简单来说就是把初始化任务一个个都封装成Task(单一职责),可以设置Task的执行线程、它需要依赖的其他task、是否需要主线程等待,最后将配置好的线程排序,按顺序分线程执行。
    图看不明白没关系,看代码:

    public class MyApplication extends Application {
    
        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));
        // 初始化CountDownLatch 值为1
        private CountDownLatch mCountDownLatch = new CountDownLatch(1);
    
        @Override
        public void onCreate() {
            super.onCreate();
            // 初始化TaskDispatcher task分发器
            TaskDispatcher.init(MyApplication.this);
            TaskDispatcher dispatcher = TaskDispatcher.createInstance();
            // 添加task 并执行
            dispatcher.addTask(new InitDelay1Task())
                    .addTask(new InitDelay2Task())
                    .addTask(new InitDelay3Task())
                    .addTask(new InitDelay4Task())
                    .start();
            // 等待一些任务执行完毕,如InitDelay2Task
            dispatcher.await();
        }
    }
    

    先不说别的,代码量和代码整洁程度就很优秀。这里我们将4个初始化任务分别封装成Task,先看看InitDelay1Task的代码

    class InitDelay1Task extends MainTask {
        @Override
        public void run() {
            // do something
        }
    }
    

    继承自MainTask,看一下MainTask:

    public abstract class MainTask extends Task {
        @Override
        public boolean runOnMainThread() {
            return true;
        }
    }
    

    意味着MainTask的派生类都是运行在主线程的的task,再看Task:

    public abstract class Task implements ITask {
        protected String mTag = getClass().getSimpleName().toString();
        protected Context mContext = TaskDispatcher.getContext();
        protected boolean mIsMainProcess = TaskDispatcher.isMainProcess();// 当前进程是否是主进程
        private volatile boolean mIsWaiting;// 是否正在等待
        private volatile boolean mIsRunning;// 是否正在执行
        private volatile boolean mIsFinished;// Task是否执行完成
        private volatile boolean mIsSend;// Task是否已经被分发
        private CountDownLatch mDepends = new CountDownLatch(dependsOn() == null ? 0 : dependsOn().size());// 当前Task依赖的Task数量(需要等待被依赖的Task执行完毕才能执行自己),默认没有依赖
    
        /**
         * 当前Task等待,让依赖的Task先执行
         */
        public void waitToSatisfy() {
            try {
                mDepends.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 依赖的Task执行完一个
         */
        public void satisfy() {
            mDepends.countDown();
        }
    
        /**
         * 是否需要尽快执行,解决特殊场景的问题:一个Task耗时非常多但是优先级却一般,很有可能开始的时间较晚,
         * 导致最后只是在等它,这种可以早开始。
         *
         * @return
         */
        public boolean needRunAsSoon() {
            return false;
        }
    
        /**
         * Task的优先级,运行在主线程则不要去改优先级
         *
         * @return
         */
        @Override
        public int priority() {
            return Process.THREAD_PRIORITY_BACKGROUND;
        }
    
        /**
         * Task执行在哪个线程池,默认在IO的线程池;
         * CPU 密集型的一定要切换到DispatcherExecutor.getCPUExecutor();
         *
         * @return
         */
        @Override
        public ExecutorService runOn() {
            return DispatcherExecutor.getIOExecutor();
        }
    
        /**
         * 异步线程执行的Task是否需要在被调用await的时候等待,默认不需要
         *
         * @return
         */
        @Override
        public boolean needWait() {
            return false;
        }
    
        /**
         * 当前Task依赖的Task集合(需要等待被依赖的Task执行完毕才能执行自己),默认没有依赖
         *
         * @return
         */
        @Override
        public List<Class<? extends Task>> dependsOn() {
            return null;
        }
    
        @Override
        public boolean runOnMainThread() {
            return false;
        }
    
        @Override
        public Runnable getTailRunnable() {
            return null;
        }
    
        @Override
        public void setTaskCallBack(TaskCallBack callBack) {}
    
        @Override
        public boolean needCall() {
            return false;
        }
    
        /**
         * 是否只在主进程,默认是
         *
         * @return
         */
        @Override
        public boolean onlyInMainProcess() {
            return true;
        }
    
        public boolean isRunning() {
            return mIsRunning;
        }
    
        public void setRunning(boolean mIsRunning) {
            this.mIsRunning = mIsRunning;
        }
    
        public boolean isFinished() {
            return mIsFinished;
        }
    
        public void setFinished(boolean finished) {
            mIsFinished = finished;
        }
    
        public boolean isSend() {
            return mIsSend;
        }
    
        public void setSend(boolean send) {
            mIsSend = send;
        }
    
        public boolean isWaiting() {
            return mIsWaiting;
        }
    
        public void setWaiting(boolean mIsWaiting) {
            this.mIsWaiting = mIsWaiting;
        }
    
    }
    

    定义了Task的属性和方法,看到这里应该明白了Task是什么。再回头来看TaskDispatcher中的start方法,添加任务后做了哪些事:

    @UiThread
        public void start() {
            mStartTime = System.currentTimeMillis();
            // 必须在主线程中执行
            if (Looper.getMainLooper() != Looper.myLooper()) {
                throw new RuntimeException("must be called from UiThread");
            }
            if (mAllTasks.size() > 0) {
                
                mAnalyseCount.getAndIncrement();
                // 打印日志
                printDependedMsg();
                // 对Task优先级进行拓扑排序
                mAllTasks = TaskSortUtil.getSortResult(mAllTasks, mClsAllTasks);
                // 初始化等待的任务数量
                mCountDownLatch = new CountDownLatch(mNeedWaitCount.get());
                // 执行子线程任务
                sendAndExecuteAsyncTasks();
    
                DispatcherLog.i("task analyse cost " + (System.currentTimeMillis() - mStartTime) + "  begin main ");
                // 执行主线程任务
                executeTaskMain();
            }
            DispatcherLog.i("task analyse cost startTime cost " + (System.currentTimeMillis() - mStartTime));
        }
    

    代码也很简单,对Task排序后分线程执行。再来看看Task之前如何绑定依赖,InitDelay4Task代码

    class InitDelay4Task extends Task {
    
        @Override
        public List<Class<? extends Task>> dependsOn() {
            List<Class<? extends Task>> tasks = new ArrayList<>();
            // 依赖于 InitDelay3Task
            tasks.add(InitDelay3Task.class);
            return tasks;
        }
    
        @Override
        public void run() {
            // do something
        }
    }
    

    重写dependsOn方法,返回需要依赖的Task列表即可。如何设置主线程等待:

    class InitDelay2Task extends Task {
    
        @Override
        public boolean needWait() {
            return true;
        }
    
        @Override
        public void run() {
            // do something
        }
    }
    

    最后贴一下拓扑排序的代码:

    public class TaskSortUtil {
    
        private static List<Task> sNewTasksHigh = new ArrayList<>();// 高优先级的Task
    
        /**
         * 任务的有向无环图的拓扑排序
         *
         * @return
         */
        public static synchronized List<Task> getSortResult(List<Task> originTasks,
                                                            List<Class<? extends Task>> clsLaunchTasks) {
            long makeTime = System.currentTimeMillis();
    
            Set<Integer> dependSet = new ArraySet<>();
            Graph graph = new Graph(originTasks.size());
            for (int i = 0; i < originTasks.size(); i++) {
                Task task = originTasks.get(i);
                if (task.isSend() || task.dependsOn() == null || task.dependsOn().size() == 0) {
                    continue;
                }
                for (Class cls : task.dependsOn()) {
                    int indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls);
                    if (indexOfDepend < 0) {
                        throw new IllegalStateException(task.getClass().getSimpleName() +
                                " depends on " + cls.getSimpleName() + " can not be found in task list ");
                    }
                    dependSet.add(indexOfDepend);
                    graph.addEdge(indexOfDepend, i);
                }
            }
            List<Integer> indexList = graph.topologicalSort();
            List<Task> newTasksAll = getResultTasks(originTasks, dependSet, indexList);
    
            DispatcherLog.i("task analyse cost makeTime " + (System.currentTimeMillis() - makeTime));
            printAllTaskName(newTasksAll);
            return newTasksAll;
        }
    
        @NonNull
        private static List<Task> getResultTasks(List<Task> originTasks,
                                                 Set<Integer> dependSet, List<Integer> indexList) {
            List<Task> newTasksAll = new ArrayList<>(originTasks.size());
            List<Task> newTasksDepended = new ArrayList<>();// 被别人依赖的
            List<Task> newTasksWithOutDepend = new ArrayList<>();// 没有依赖的
            List<Task> newTasksRunAsSoon = new ArrayList<>();// 需要提升自己优先级的,先执行(这个先是相对于没有依赖的先)
            for (int index : indexList) {
                if (dependSet.contains(index)) {
                    newTasksDepended.add(originTasks.get(index));
                } else {
                    Task task = originTasks.get(index);
                    if (task.needRunAsSoon()) {
                        newTasksRunAsSoon.add(task);
                    } else {
                        newTasksWithOutDepend.add(task);
                    }
                }
            }
            // 顺序:被别人依赖的————》需要提升自己优先级的————》需要被等待的————》没有依赖的
            sNewTasksHigh.addAll(newTasksDepended);
            sNewTasksHigh.addAll(newTasksRunAsSoon);
            newTasksAll.addAll(sNewTasksHigh);
            newTasksAll.addAll(newTasksWithOutDepend);
            return newTasksAll;
        }
    
        private static void printAllTaskName(List<Task> newTasksAll) {
            if (true) {
                return;
            }
            for (Task task : newTasksAll) {
                DispatcherLog.i(task.getClass().getSimpleName());
            }
        }
    
        public static List<Task> getTasksHigh() {
            return sNewTasksHigh;
        }
    
        /**
         * 获取任务在任务列表中的index
         *
         * @param originTasks
         * @param taskName
         * @return
         */
        private static int getIndexOfTask(List<Task> originTasks,
                                          List<Class<? extends Task>> clsLaunchTasks, Class cls) {
            int index = clsLaunchTasks.indexOf(cls);
            if (index >= 0) {
                return index;
            }
    
            // 仅仅是保护性代码
            final int size = originTasks.size();
            for (int i = 0; i < size; i++) {
                if (cls.getSimpleName().equals(originTasks.get(i).getClass().getSimpleName())) {
                    return i;
                }
            }
            return index;
        }
    }
    

    2、延迟优化

    项目中有些初始化任务并不是启动时就需要初始化的(注:这里我们只考虑需要在主线程初始化的任务),这时我们就考虑将它们延时加载,加载时机选择在第一条真实数据展示之后,例如首页列表第一条数据加载结束。

    延迟方式
    一般的延迟方式会使用 Handler.postDelay 或者View.postDelay,但是这种延时任务进入消息队列后会阻塞住用户后续的UI操作,导致卡顿的现象,所以我们介绍一种利用 IdelHandler 的延迟方式。
    IdelHandler:当主线程的消息队列中没有消息需要处理时,就会调取IdelHandler中的消息过来处理。

    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();
                }
                // 如果任务队列为空就移除掉这个 idle handler
                return !mDelayTasks.isEmpty();
            }
        };
    
        public DelayInitDispatcher addTask(Task task){
            mDelayTasks.add(task);
            return this;
        }
    
        public void start(){
            // 加入当前线程的队列
            Looper.myQueue().addIdleHandler(mIdleHandler);
        }
    
    }
    

    主要逻辑就是将任务封装成Task对象,创建一个Task队列,每次 IdleHandler 中的消息被回调就从Task队列中取出一个Task出来执行,分批执行,避免一次执行任务过多时间过长的情况。
    调用也很简单:

    DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
    delayInitDispatcher.addTask(new DelayInitTaskA())
                .addTask(new DelayInitTaskB())
                .start();
    

    注意:利用 IdelHandler 执行的任务一般不会影响用户的UI操作,当然这并不绝对,如果任务耗时很久则需要换一个时机处理(如懒加载),或者开始执行任务的一瞬间用户刚好操作了界面,这就需要做取舍了,具体场景具体对待,这里只是介绍一种方式。

    3、其他优化方式

    除了异步优化、延迟优化还有懒加载等优化方式,例如,如果地图功能只有详情页需要就可以把地图放在详情页再初始化。每一种优化的方式使用场景都不绝对,我们要根据具体的业务需求提出具体的优化方案,酌情做出取舍,让优化发挥最大效益。

    相关文章

      网友评论

          本文标题:启动优化

          本文链接:https://www.haomeiwen.com/subject/ibvvbctx.html