美文网首页
Android启动优化

Android启动优化

作者: 咸鱼Jay | 来源:发表于2022-04-26 20:43 被阅读0次

    启动优化之有向无环图任务管理

    启动性能,是用户在使用APP过程中的第一感观,可见是相当重要的。相信很多人都能说出一些常规的手段,比如只加载必要的模块,延迟加载等。从大的策略上说,是没有问题的,也能够获取一些效果,但仍存在一些问题。

    为什么需要启动框架?

    对于很多APP而言,因为启动包含很多基础SDK,SDK的初始化有着一定的先后顺序;业务SDK又是围绕着多个基础 SDK建立的。那么如何保证这些SDK在正确的阶段、按照正确的依赖顺序、高效地初始化?怎么合理调度任务,才不至于让系统负载过高?

    比如我们存在5个初始化任务,这5个任务之间存在如下图的依赖关系:


    初始化任务1先执行,任务2任务3需要等待任务1执行完成,任务4需要等待任务2执行完成,任务3任务4执行完成后,任务5才能执行。

    面对上述初始化任务之间复杂的依赖场景,我们应该如何解决?
    此时我们按照任务的依赖关系,主动按照任务执行顺序调用:

    new Thread(){
       public void run(){
           SDK1.init();
           SDK2.init();
           SDK3.init();
           SDK4.init();
           SDK5.init();
       }
    }
    

    这样做的话就会导致任务3需要等待任务2执行完毕才能得到执行!
    因此我们可以将任务2放入单独的异步线程完成初始化:

    new Thread(){
        public void run(){
            SDK1.init();
            // 单独异步执行
            new Thread(){
                @Override
                public void run() {
                    SDK2.init();
                }
            }.start();
            SDK3.init();
            SDK4.init();// 可能先于SDK2完成前执行
            SDK5.init();
        }
    }
    

    根据依赖关系得知:任务4需要等待任务2执行完成。但是上述代码又可能会导致任务2未完成,任务4就开始执行的情况。
    那我们再次改变:

    new Thread(){
        public void run(){
            SDK1.init();
            Thread t2 = new Thread(){
                @Override
                public void run() {
                    SDK2.init();
                }
            };
            t2.start();
            SDK3.init();
            t2.join();//join等待SDK2初始化完成
            SDK4.init();
            SDK5.init();
        }
    }
    

    此时如果任务2先执行完成,应该马上执行任务4,但是上述代码中任务4需要等待任务3执行完成。

    最后我们发现,不管我们怎么做,都会遇到不同的问题。并且当我们的初始任务更多,关系更为复杂时,此时手动管理我们的任务执行,变得极为繁琐且容易出错。而且面对千变万化的需求,一旦启动任务发生变化(新增、删除、依赖改变)如果没有任何设计,那将“牵一发而动全身"所以此时,我们需要一个启动框架,帮助我们完成启动任务的管理与调度。

    启动框架设计

    其实启动框架就是一个任务调度系统,要做的事情就是把初始任务之间的关系梳理得明明白白,有条不紊,合理安排位置、调度时间,同时提升硬件资源的利用率。

    任务管理

    在我们应用端不改变现有启动任务执行逻辑的前提下进行启动优化,本质上就是解决任务的依赖性问题,即先执行什么,再执行什么。而依赖性问题的本质就是数据结构的问题。

    DAG有向无环图

    我们根据启动任务之间的关系,绘制对应的图示如下:



    在上图中,任务的执行有方向(有序),且没有回环。在图论中,这种一个有向图无法从某个顶点出发经过若干条边回到该点,那么这个图就是一个有向无环图,简称DAG图。DAG常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。

    在一个DAG中:

    • 顶点:图中的一个点,比如任务1,任务2;
    • 边:连接两个顶点的线段叫做边;
    • 入度:代表当前有多少边指向顶点(依赖多少任务);
    • 出度:代表有多少边从顶点发出(被多少任务依赖)。

    拓扑排序

    在将我们的启动任务绘制完成DAG之后,我们接下来,就需要求出DAG的拓扑序列,即对我们的启动任务执行顺序进行排序。

    对于上文中的任务依赖关系来说,我们只需要保证2与3在1之后执行,4在2之后,5在3、4任务之后执行即可。因此我们可以得到排序后的结果为:

    • 1->2->3->4->5
    • 1-> 2->4-> 3->5
    • 1 ->3->2->4->5

    因此图的拓扑排序不是唯一的!只要符合以下两点要求即可:
    每个顶点出现且只出现一次。
    若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面

    对DAG进行拓扑排序,我们可以选择BFS(广度优先)或者DFS(深度优先)。利用BFS的算法排序的过程如下:

    1. 找出图中0入度的顶点;
    2. 依次在图中删除这些顶点,删除后再找出0入度的顶点;
    3. 删除后再找出0入度的顶点,重复执行第二步

    入度为0的顶点为任务1,得到结果:1



    删除任务1后,此时任务2与任务3入度数由1变为0,得到结果:1->2->3



    删除任务2后,任务4入度数由1变为O;删除任务3后,任务5入度数由2变为1,得到结果: 1->2-> 3
    删除任务4后,任务5入度数由1变为0,得到结果:1->2->3->4->5

    可在此处点击查看排序的动画过程:点击查看

    代码实现

    根据之前的场景,我们设计Startup接口:

    public interface Startup<T> {
    
        /**
         * 执行初始化任务
          */
        T create(Context context);
    
        /**
         * 本任务依赖哪些任务
         */
        List<Class<? extends Startup<?>>> dependencies();
    
        /**
         * 依赖任务的个数(入度数)
          */
        int getDependenciesCount();
    }
    

    同时提供一个AndroidStartup抽象类,此抽象类目前的作用很简单,根据dependencies实现getDependenciesCount方法:

    public abstract class AndroidStartup<T> implements Startup<T>{
    
        @Override
        public List<Class<? extends Startup<?>>> dependencies() {
            return null;
        }
    
        @Override
        public int getDependenciesCount() {
            List<Class<? extends Startup<?>>> dependencies = dependencies();
            return dependencies == null ? 0 :dependencies.size();
        }
    }
    

    基于上述的接口与抽象类,我们可以定义自己的各个启动任务类:

    public class Task1 extends AndroidStartup<String>{
    
        @Override
        public String create(Context context) {
            return "Task1返回数据";// 执行初始化
        }
    }
    
    public class Task2 extends AndroidStartup<Void>{
    
        private List<Class<? extends Startup<?>>> depends;
    
        public Task2(){
            depends = new ArrayList<>();
            //本任务依赖于任务1
            depends.add(Task1.class);
        }
    
        @Override
        public Void create(Context context) {
            return null;// 执行初始化
        }
    
        @Override
        public List<Class<? extends Startup<?>>> dependencies() {
            return depends;
        }
    }
    

    最后我们完成拓扑排序的代码实现:

    1、找出入度为0的任务

    在这一步,我们同时记录了如下表:


    创建一个拓扑排序类TopologySort,实现拓扑排序sort方法

    // TopologySort.java
    public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
        Map<Class<? extends Startup>, Integer> inDegreeMap = new HashMap<>();
        Deque<Class<? extends Startup>> zeroDeque = new ArrayDeque<>();
    
        Map<Class<? extends Startup>, Startup<?>> startupMap = new HashMap<>();
        Map<Class<? extends Startup>, List<Class<? extends Startup>>> startupChildrenMap = new HashMap<>();
    
        // 1、找出图中0入度的顶点
        for (Startup<?> startup : startupList) {
            startupMap.put(startup.getClass(), startup);
            //记录每个任务的入度数(依赖的任务数)
            int dependenciesCount = startup.getDependenciesCount();
            inDegreeMap.put(startup.getClass(),dependenciesCount);
            //记录入度数(依赖的任务数)为0的任务
            if(dependenciesCount == 0){
                zeroDeque.offer(startup.getClass());
            }else {
                //遍历本任务的依赖(父)任务列表
                for (Class<? extends Startup<?>> parent : startup.dependencies()) {
                    List<Class<? extends Startup>> children = startupChildrenMap.get(parent);
                    if(children == null){
                        children = new ArrayList<>();
                        //记录这个父任务的所有子任务
                        startupChildrenMap.put(parent,children);
                    }
                    children.add(startup.getClass());
                }
            }
        }
    
        ......
    }
    

    2、删除入度为0的任务

    // TopologySort.java
    public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
      
        ......
    
        // 2.1、依次在图中删除这些顶点
        List<Startup<?>> result = new ArrayList<>();// 排序结果
        //处理入度为0的任务
        while (!zeroDeque.isEmpty()) {
            Class<? extends Startup> cls = zeroDeque.poll();
            Startup<?> startup = startupMap.get(cls);
            result.add(startup);
    
            // 2.2、删除后再找出现在0入度的顶点
            if(startupChildrenMap.containsKey(cls)){
                List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
                for (Class<? extends Startup> childCls : childStartup) {
                    Integer num = inDegreeMap.get(childCls);
                    inDegreeMap.put(childCls,num - 1);// 入度数-1
                    if(num -1 == 0){
                        zeroDeque.offer(childCls);
                    }
                }
            }
        }
        return new StartupSortStore(result, startupMap, startupChildrenMap);
    }
    

    在这一步中, 我们首先删除入度为0的: Task1并将其记录在结果集合result中; 删除Task1之后,通过任务依赖表: startupChildrenMap, 找到Task2与Task3:

    然后从入度数表: inDegreeMap中把Task2与Task3的入度数减一:


    如果发现,Task2/Task3 减- -后入度数变为0,则将其加入零入度队列: zeroDeque:

    继续循环,直到处理完成。这就是利用广度搜索实现拓扑排序的过程!

    3、创建StartupCacheManager类

    创建StartupCacheManager类,缓存每个任务执行的结果

    public class StartupCacheManager {
        
        private ConcurrentHashMap<Class<? extends Startup>, Result> mInitializedComponents = new ConcurrentHashMap();
    
        private static StartupCacheManager mInstance;
    
        private StartupCacheManager() {}
    
        public static StartupCacheManager getInstance() {
            if (mInstance == null) {
                synchronized (StartupCacheManager.class) {
                    if (mInstance == null) {
                        mInstance = new StartupCacheManager();
                    }
                }
            }
            return mInstance;
        }
    
        public void saveInitializedComponent(Class<? extends Startup> zClass, Result result) {
            mInitializedComponents.put(zClass, result);
        }
    
        public boolean hadInitialized(Class<? extends Startup> zClass) {
            return mInitializedComponents.containsKey(zClass);
        }
    
        public <T> Result<T> obtainInitializedResult(Class<? extends Startup<T>> zClass) {
            return mInitializedComponents.get(zClass);
        }
    
    
        public void remove(Class<? extends Startup> zClass) {
            mInitializedComponents.remove(zClass);
        }
    
        public void clear() {
            mInitializedComponents.clear();
        }
    }
    

    上面ConcurrentHashMap的范型为啥用的是Result类?
    是为了防止空指针异常,因为ConcurrentHashMapput方法的keyvalue都不能为null

    public class Result<T> {
    
        public T data;
    
        public Result(T data) {
            this.data = data;
        }
    }
    

    4、创建StartupManager类

    public class StartupManager {
    
        private Context context;
        private List<AndroidStartup<?>> startupList;
    
        public StartupManager(Context context, List<AndroidStartup<?>> startupList) {
            this.context = context;
            this.startupList = startupList;
        }
    
        public StartupManager start() {
            if(Looper.myLooper() != Looper.getMainLooper()){
                throw new RuntimeException("请在主线程调用!");
            }
            StartupSortStore startupSortStore = TopologySort.sort(startupList);
            for (Startup<?> startup : startupSortStore.getResult()) {
                Object result = startup.create(context);
                StartupCacheManager.getInstance().saveInitializedComponent(startup.getClass(),new Result(result));
            }
            return this;
        }
    
        public static class Builder{
            private List<AndroidStartup<?>> startupList = new ArrayList<>();
    
            public Builder addStartup(AndroidStartup<?> startup) {
                startupList.add(startup);
                return this;
            }
    
            public Builder addAllStartup(List<AndroidStartup<?>> startups) {
                startupList.addAll(startups);
                return this;
            }
    
            public StartupManager build(Context context) {
                return new StartupManager(context, startupList);
            }
        }
    }
    

    小结:

    1. 你是如何处理Android的启动优化的?
      不改变现有启动任务执行逻辑的前提下,启动优化本质上就是解决任务的依赖性问题,依赖问题本质就是数据结构问题!
    2. 什么是有向无环图,拓扑排序?
      一个有向图无法从某个顶点出发经过若干条边回到该点,那么这个图就是一个有向无环图,简称DAG图。DAG常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。拓扑排序是对一个有向图构造拓扑序列的过程。

    线程管理

    启动任务经过基于DAG的拓扑排序后能够有序执行了,但是我们在程序启动的时候所有的初始化任务难道都一定需要在主线程阻塞主线程来初始化吗?所以此时我们就不得不考虑加入线程管理的模块,那么现在我们遇到这样的一个需求:五个任务现在我们都要放入子线程进行初始化执行,同时又要保证各个任务之间的执行顺序这时候我们该怎么办?

    我们来看看这个面试过程:
    Q:假设有A、B两个线程,B线程需要在A线程执行完成之后执行。
    A:

    Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("执行第一个线程任务!");
        }
    };
    t1.start();
    t1.join(); // 阻赛等待线程1执行完毕
    Thread t2 = new Thread(){
        @Override
        public void run() {
            System.out.println("执行第二个线程任务!");
        }
    };
    t2.start();
    

    Q:假设有A、B两个线程,其中A线程中执行分为3步,需要在A线程执行完成第二步之后再继续执行B线程的代码怎么办?
    A:

    Object lock = new Object();
    Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("第一步执行完成!");
            System.out.println("第二步执行完成!");
            synchronized (lock) {
                lock.notify();
            }
            System.out.println("第三步执行完成!");
        }
    };
    
    Thread t2 = new Thread(){
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("执行第二个线程任务!");
        }
    };
    t2.start();
    t1.start();
    

    Q:假设有A. B、C三个线程,其中A、B线程执行分为三步,C线程,需要在A线程与B线程都执行到第二步时才能
    A:

    Object lock1 = new Object();
    Object lock2 = new Object();
    Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("t1:第一步执行完成!");
            System.out.println("t1:第二步执行完成!");
            synchronized (lock1) {
                lock1.notify();
            }
            System.out.println("t1:第三步执行完成!");
        }
    };
    Thread t2 = new Thread(){
        @Override
        public void run() {
            System.out.println("t2:第一步执行完成!");
            System.out.println("t2:第二步执行完成!");
            synchronized (lock2) {
                lock2.notify();
            }
            System.out.println("t2:第三步执行完成!");
        }
    };
    Thread t3 = new Thread(){
        @Override
        public void run() {
            synchronized (lock1) {
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lock2) {
                try {
                    lock2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("执行第三个线程任务!");
        }
    };
    t3.start();
    t2.start();
    t1.start();
    

    在上面的问答过程中,最后一个问题:

    假设有A、B、C三个线程,其中A. B线程执行分为三步, C线程,需要在A线程与B线程都执行到第一步时才能执行,怎么办?

    根据这位面试者的回答,线程3先等待线程1的通知,再等待线程2的通知,才能得到执行。但是,如果线程2的通知先于线程1的通知到达。那么此时,线程3将-直被阻塞, 因为线程1已经发出过通知了。

    那么面对上述问题,我们需要采用闭锁-- CountDownLatch就能够很好的解决此问题:


    CountDownLatch在初始化时,需要指定一个状态值, 可以看成一个计数器。

    当我们调用await方法,若状态值为0则不会发生阻塞,否则会阻塞。而在调用countDown方法后,会利用CAS机制将状态值-1,直到状态值为0, await将不再阻塞!

    CountDownLatch countDownLatch = new CountDownLatch(2);
    Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("t1:第一步执行完成!");
            System.out.println("t1:第二步执行完成!");
            countDownLatch.countDown();
            System.out.println("t1:第三步执行完成!");
        }
    };
    Thread t2 = new Thread(){
        @Override
        public void run() {
            System.out.println("t2:第一步执行完成!");
            System.out.println("t2:第二步执行完成!");
            countDownLatch.countDown();
            System.out.println("t2:第三步执行完成!");
        }
    };
    Thread t3 = new Thread(){
        @Override
        public void run() {
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行第三个线程任务!");
        }
    };
    t3.start();
    t2.start();
    t1.start();
    

    执行结果:

    t2:第一步执行完成!
    t1:第一步执行完成!
    t2:第二步执行完成!
    t1:第二步执行完成!
    t2:第三步执行完成!
    t1:第三步执行完成!
    执行第三个线程任务!
    

    因此我们改造第一步创建的接口与抽象类增加:

    public interface Dispatcher {
        /**
         * 是否在主线程执行
         */
        boolean callCreateOnMainThread();
    
        /**
         * 是否需要等待该任务执行完成
         * 只有callCreateOnMainThread方法返回false的时候才有意义
         */
        boolean waitOnMainThread();
    
        /**
         * 等待
         */
        void toWait();
    
        /**
         * 有父任务执行完毕
         * 计数器-1
         */
        void toNotify();
    
        /**
         * 若在子线程执行,则指定线程池
         */
        Executor executor();
    
        /**
         * 线程优先级
         */
        int getThreadPriority();
    }
    
    public interface Startup<T> extends Dispatcher {//继承Dispatcher
        ....
    }
    
    public abstract class AndroidStartup<T> implements Startup<T>{
        // 根据入度数(依赖任务的个数)创建闭锁
        private CountDownLatch mWaitCountDown = new CountDownLatch(getDependenciesCount());
    
        @Override
        public List<Class<? extends Startup<?>>> dependencies() {
            return null;
        }
    
        @Override
        public int getDependenciesCount() {
            List<Class<? extends Startup<?>>> dependencies = dependencies();
            return dependencies == null ? 0 :dependencies.size();
        }
    
        @Override
        public Executor executor() {
            return ExecutorManager.ioExecutor;
        }
    
        //执行此任务时,调用toWait
        @Override
        public void toWait() {
            try {
                mWaitCountDown.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        // 当前无依赖的任务执行完成后,需要调用本任务的toNotify
        @Override
        public void toNotify() {
            mWaitCountDown.countDown();
        }
    
        @Override
        public int getThreadPriority() {
            return Process.THREAD_PRIORITY_DEFAULT;
        }
    }
    
    public class ExecutorManager {
    
        public static ThreadPoolExecutor cpuExecutor;
        public static ExecutorService ioExecutor;
        public static Executor mainExecutor;
    
        //获得CPU核心数
        private static int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        private static int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));
        private static int MAX_POOL_SIZE = CORE_POOL_SIZE;
        private static long KEEP_ALIVE_TIME = 5L;
    
        static {
            cpuExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                    new LinkedBlockingDeque<Runnable>(),
                    Executors.defaultThreadFactory());
            cpuExecutor.allowCoreThreadTimeOut(true);
            ioExecutor = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
            mainExecutor = new Executor() {
                Handler handler = new Handler(Looper.getMainLooper());
    
                @Override
                public void execute(Runnable command) {
                    handler.post(command);
                }
            };
        }
    }
    

    至此我们借助闭锁-- CountDownLatch很好的解决了线程同步问题!

    现在StartupManager类的start方法需要支持主线程和子线程判断进行执行

    public class StartupManager {
        ......
        public StartupManager start() {
            if(Looper.myLooper() != Looper.getMainLooper()){
                throw new RuntimeException("请在主线程调用!");
            }
            startupSortStore = TopologySort.sort(startupList);
            for (Startup<?> startup : startupSortStore.getResult()) {
                StartupRunnable startupRunnable = new StartupRunnable(context,startup,this);
                if(startup.callCreateOnMainThread()){
                    startupRunnable.run();
                }else {
                    startup.executor().execute(startupRunnable);
                }
            }
            return this;
        }
    
        public void notifyChildren(Startup<?> startup) {
            //获得已经完成的当前任务的所有子任务
            if(startupSortStore.getStartupChildrenMap().containsKey(startup.getClass())){
                List<Class<? extends Startup>> childStartupCls = startupSortStore.getStartupChildrenMap().get(startup.getClass());
                for (Class<? extends Startup> cls : childStartupCls) {
                    // 通知子任务 startup父任务已完成
                    Startup<?> childStartup = startupSortStore.getStartupMap().get(cls);
                    childStartup.toNotify();
                }
            }
        }
        ......
    }
    

    这里封装一个RunnableStartupRunnable,用于执行任务

    public class StartupRunnable implements Runnable{
    
        private StartupManager startupManager;
        private Startup<?> startup;
        private Context context;
    
        public StartupRunnable(Context context, Startup<?> startup,
                               StartupManager startupManager) {
            this.context = context;
            this.startup = startup;
            this.startupManager = startupManager;
        }
    
        @Override
        public void run() {
            Process.setThreadPriority(startup.getThreadPriority());
            startup.toWait();
            Object result = startup.create(context);
            StartupCacheManager.getInstance().saveInitializedComponent(startup.getClass(),new Result(result));
            startupManager.notifyChildren(startup);
        }
    }
    

    这里又调用StartupManager类的notifyChildren方法,用于通知当前任务的子任务去执行

    到现在为止,任务不管在子主线程还是子线程,都能有序的执行起来。现在可以在项目中Application里使用起来:

    new StartupManager.Builder()
            .addStartup(new Task5())
            .addStartup(new Task4())
            .addStartup(new Task3())
            .addStartup(new Task2())
            .addStartup(new Task1())
            .build(this)
            .start();
    

    执行结果:


    思考:如何任务5是在子线程,主线程需要等待任务5执行完成,主线程才能继续执行,怎么办?

    StartupManager.Builder类的build方法里使用一个整形数据AtomicInteger记录多少个在子线程执行,又要主线程等待的任务数

    public StartupManager build(Context context) {
        AtomicInteger needAwaitCount = new AtomicInteger();
        for (AndroidStartup<?> startup : startupList) {
            //记录需要主线程等待完成的异步任务
            if (!startup.callCreateOnMainThread() && startup.waitOnMainThread()) {
                needAwaitCount.incrementAndGet();
            }
        }
        CountDownLatch awaitCountDownLatch = new CountDownLatch(needAwaitCount.get());
        return new StartupManager(context, startupList,awaitCountDownLatch);
    }
    

    然后在StartupManager类里新增await方法和notifyChildren里新增awaitCountDownLatch.countDown();

    public void await(){
        try {
            awaitCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public void notifyChildren(Startup<?> startup) {
        if(!startup.callCreateOnMainThread() && startup.waitOnMainThread()) {
            awaitCountDownLatch.countDown();
        }
        //获得已经完成的当前任务的所有子任务
        if(startupSortStore.getStartupChildrenMap().containsKey(startup.getClass())){
            List<Class<? extends Startup>> childStartupCls = startupSortStore.getStartupChildrenMap().get(startup.getClass());
            for (Class<? extends Startup> cls : childStartupCls) {
                // 通知子任务 startup父任务已完成
                Startup<?> childStartup = startupSortStore.getStartupMap().get(cls);
                childStartup.toNotify();
            }
        }
    }
    
    public class App extends Application {
    
        @Override
        public void onCreate() {
            super.onCreate();
            LogUtils.log("onCreate->start");
            new StartupManager.Builder()
                    .addStartup(new Task5())
                    .addStartup(new Task4())
                    .addStartup(new Task3())
                    .addStartup(new Task2())
                    .addStartup(new Task1())
                    .build(this)
                    .start().await();
            LogUtils.log("onCreate->end");
        }
    }
    

    打印结果:


    框架自动配置

    上面我们在应用的Application里使用StartupManager.Builder()添加多个任务,如果这个任务特别多,就会显着特别的繁琐,有什么办法优化呢?
    其实可以使用1、注解,2、字节码插桩,3、路由等都可以优化这块。

    问题:LeakCanary为什么不需要在Application中手动初始化?
    因为LeakCanary是使用了ContentProviders
    所以我们这里也可以使用ContentProviders

    回顾一下Android的启动流程:
    在调用ApplicationattachBaseContextcallAppcationOnCreate之间ActivityThread里面回去创建ContentPovider, 执行你在AndroidManifest里面所注册的ContentProvidersCreate方法。


    我们可以利用这个知识点
    public class StartupProvider extends ContentProvider {
        @Override
        public boolean onCreate() {
            try {
                List<Startup<?>> startups = StartupInitializer.discoverAndInitializ(getContext(), getClass().getName());
                new StartupManager.Builder()
                        .addAllStartup(startups)
                        .build(getContext())
                        .start()
                        .await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
        ......
    }
    
    public class StartupInitializer {
        public static String META_VALUE = "android.startup";
    
        public static List<Startup<?>> discoverAndInitializ(Context context, String providerName) throws Exception {
            Map<Class<? extends Startup>, Startup<?>> startups = new HashMap<>();
            //获得manifest contentProvider中的meta-data
            ComponentName provider = new ComponentName(context, providerName);
            ProviderInfo providerInfo = context.getPackageManager().getProviderInfo(provider,
                    PackageManager.GET_META_DATA);
            //得到manifest中配置的任务
            for (String key : providerInfo.metaData.keySet()) {
                String value = providerInfo.metaData.getString(key);
                if (TextUtils.equals(META_VALUE, value)) {
                    Class<?> clazz = Class.forName(key);
                    if (Startup.class.isAssignableFrom(clazz)) {
                        doInitialize((Startup<?>) clazz.newInstance(), startups);
                    }
                }
            }
    
            List<Startup<?>> result = new ArrayList<>(startups.values());
            return result;
        }
    
        private static void doInitialize(Startup<?> startup, Map<Class<? extends Startup>, Startup<?>> startups) throws Exception {
            startups.put(startup.getClass(), startup);
            if (startup.getDependenciesCount() != 0) {
                //遍历父任务
                for (Class<? extends Startup<?>> dependency : startup.dependencies()) {
                    doInitialize(dependency.newInstance(), startups);
                }
            }
        }
    }
    
    <provider
        android:authorities="${applicationId}.android_startup"
        android:name=".provider.StartupProvider"
        android:exported="false">
        <meta-data android:name="com.zxj.appstartup.tasks.Task5"
            android:value="android.startup"/>
    </provider>
    

    这里的meta-data指定的是Task5,然后通过ContentProviders解析出来,因为任务都是有关联的,所以这里拿到Task5后也可以拿到其他的任务

    拓扑优化-阻塞问题的解决

    目前为止我们解决了任务的执行顺序问题与线程管理问题,但是如果我们面临这样的一一个场景怎么办?


    任务2必须在主线程执行,其他任务在子线程执行。

    DAG的拓扑序列会有多个:12345、12435、13245
    若我们排序得出的拓扑序列为: 12345

    异步任务1执行完成,由于同步任务2需要在主线程执行,此时异步任务3只能等待同步任务2执行完成才能得到分发执行!

    面对上述的场景,由于任务3需要等待任务2执行完成,造成我们无法合理的运用多线程的资源,对应用启动速度没有实现彻底的优化。此时我们需要改造我们的拓扑排序。

    在拓扑排序代码实现中的第二步,我们将代码改为:

    public class TopologySort {
    
        public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
            ......
    
            // 2.1、依次在图中删除这些顶点
            List<Startup<?>> result = new ArrayList<>();// 排序结果
            List<Startup<?>> main = new ArrayList<>();//主线程执行的任务
            List<Startup<?>> threads = new ArrayList<>();//子线程执行的任务
            //处理入度为0的任务
            while (!zeroDeque.isEmpty()) {
                Class<? extends Startup> cls = zeroDeque.poll();
                Startup<?> startup = startupMap.get(cls);
                // 修改
                if(startup.callCreateOnMainThread()){
                    main.add(startup);
                }else {
                    threads.add(startup);
                }
    
                // 2.2、删除后再找出现在0入度的顶点
                if(startupChildrenMap.containsKey(cls)){
                    List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
                    for (Class<? extends Startup> childCls : childStartup) {
                        Integer num = inDegreeMap.get(childCls);
                        inDegreeMap.put(childCls,num - 1);
                        if(num -1 == 0){
                            zeroDeque.offer(childCls);
                        }
                    }
                }
            }
            //先添加子线程到result
            result.addAll(threads);
            result.addAll(main);
            return new StartupSortStore(result, startupMap, startupChildrenMap);
        }
    }
    

    经过修改后,如果之前排序结果为:12345,那么将变为:13452。
    此时执行流程为︰
    Task1 ->子线程执行
    Task3->子线程等待Task1
    Task4->子线程等待Task2
    Task5->子线程等待Task3+Task4
    Task2 ->主线程等待Task1

    • 若Task1执行完成,那么Task2与Task3将分别在主线程与子线程执行;
    • 若Task3执行完成,Task5将继续等待Task4;
    • Task4在等待Task2执行完成,Task2执行完成后,通知Task4执行(toNotify);
    • Task4执行完成,Task5执行。

    因此实际上,我们实现对子线程任务与主线程任务分别拓扑排序,先分发所有子线程任务,再执行主线程任务,同样满足任务执行顺序。

    源码链接

    相关文章

      网友评论

          本文标题:Android启动优化

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