如何应对 Android 面试官 -> 启动如何进行优化(下)?玩转 Android StartUp

前言


在这里插入图片描述

本章继续上一章的启动优化讲解,主要基于手淘全链路性能优化分析 Android StartUp 启动框架;

有向无环图(DAG)算法


我们首先去 LeetCode 上看一道算法题:课程表

在这里插入图片描述
看题目,可能好多算法薄弱的人有点懵逼,转换一下思路,例如:如果你想学习 OKHttp,那么你需要一些其他的知识来辅助你学习 OKhttp
在这里插入图片描述

那么你需要学习 java 只是,学习 Socket、Https 和设计模式,然后学习了这些知识之后,来辅助你学习 OKHttp,这样的一个依赖关系;

转换到我们进行启动优化上,我们在 Application 中初始化三方 SDK 的时候,就要考虑相互之间的依赖关系,而不是一股脑的所有 SDK 都放到子线程或者延迟初始化;

这种有依赖,有方向,没有形成回环的结构就是『有向无环图』简称 DAG;

在上面那张依赖图中,剪头我们通常叫做『边』,Socket 有一个箭头指向它,那么它的入度就是 1,OKHttp 有两个箭头指向它,那么它的入度就是 2,倒过来讲就是出度,Java 的出度是 2, OKHttp 的出度是 0;

那么,我们接下来如何通过代码来实现这个『有向无环图』我们可以借助 BFS 或者 DFS,对这两个感兴趣的可以看下这个链接:图文详解 BFS DFS

拓扑排序

拓扑排序是对一个有向图构造拓扑序列的过程;图的拓扑排序不是唯一的;实现拓扑排序的整体思路是:

  • 找出图中 0 入度的顶点;
  • 依次在图中删除这些顶点,删除后再找出图中 0 入度的顶点;
  • 然后在删除…再找出…;
  • 直至删除所有顶点,即完成拓扑排序;
    在这里插入图片描述
    在这里插入图片描述

Android StartUp 具体实现


首先我们来定一个 StartUp 接口,这个接口用来创建当前任务,获取当前任务依赖了哪些任务,以及当前任务依赖了多少个任务

任务的排序

public interface Startup<T> {

    /**
     * 创建当前任务
     * @param context context
     * @return T 任务执行结果,返回值类型
     */
    T create(Context context);

    /**
     * 当前任务依赖了哪些任务,也就是入度数
     *
     * @return List<Class<? extends StartUp<?>>>
     */
    List<Class<? extends Startup<?>>> dependencies();

    /**
     * 当前任务依赖了多少个任务
     * @return int
     */
    int getDependenciesCount();
}

然后定义一个抽象类 AndroidStartup 实现 Startup 接口

public abstract class AndroidStartup<T> implements Startup<T> {
    
    /**
     * 获取依赖的任务
     * @return 依赖的任务
     */
    @Override
    public List<Class<? extends Startup<?>>> dependencies() {
        return null;
    }

    /**
     * 获取依赖的任务数
     * @return int
     */
    @Override
    public int getDependenciesCount() {
        List<Class<? extends Startup<?>>> dependencies = dependencies();
        return dependencies == null ? 0 : dependencies.size();
    }
}

这个抽象类中只实现了 dependencies 和 getDependenciesCount 接口;

然后定义了 5 个 Task;
在这里插入图片描述

CommonParamsTaskStartup 依赖 PushTaskStartup;

CuidTaskStartup 依赖 PushTaskStartup;

DeviceIdTaskStartup 依赖 CommonParamsTaskStartup

ImTaskStartup 依赖 DeviceIdTaskStartup、CuidTaskStartup

PushTaskStartup 没有任何依赖;

所以最终的有向无环图如下:
在这里插入图片描述

那么,具体是怎么实现这个拓扑排序的呢?我们来看下排序规则:

/**
 * 拓扑排序
 * @param startupList startupList
 * @return StartupSortStore
 */
public static StartupSortStore sort(List<? extends Startup<?>> startupList) {

    // 将所有的任务存入到一个map中
    Map<Class<? extends Startup>, Startup<?>> startupMap = new HashMap<>();

    // 入读表,记录每个任务的依赖数
    Map<Class<? extends Startup>, Integer> inDegreeMap = new HashMap<>();
    // 记录每个任务的依赖任务
    Map<Class<? extends Startup>, List<Class<? extends Startup>>> startupChildrenMap = new HashMap<>();
    // 记录入度为 0 的任务
    Deque<Class<? extends Startup>> zeroDegree = new ArrayDeque<>();
    // 遍历所有的任务
    for (Startup<?> startup : startupList) {
        startupMap.put(startup.getClass(), startup);
        // 获取每个任务的依赖数
        int dependenciesCount = startup.getDependenciesCount();
        // 存入入度表中
        inDegreeMap.put(startup.getClass(), dependenciesCount);

        if (dependenciesCount == 0) {
            // 如果依赖的任务数为0,则存入0入度表
            zeroDegree.offer(startup.getClass());
        } else {
            // 获取每个任务依赖的任务,一个任务可以依赖多个任务,所以是一个集合
            // 例如获取 ImTaskStartup 的 dependencies,包含 DeviceIdTaskStartup CuidTaskStartup
            List<Class<? extends Startup<?>>> dependencies = startup.dependencies();
            // 遍历依赖的任务
            for (Class<? extends Startup<?>> parent : dependencies) {
                // parent = DeviceIdTaskStartup | parent = CuidTaskStartup
                List<Class<? extends Startup>> children = startupChildrenMap.get(parent);
                if (children == null) {
                    children = new ArrayList<>();
                    startupChildrenMap.put(parent, children);
                }
                // 也就是说 DeviceIdTaskStartup 的 child 是 ImTaskStartup
                children.add(startup.getClass());
            }
        }
    }
    // 依次
    List<Startup<?>> result = new ArrayList<>();
    // 如果 0 入度表不为空
    while (!zeroDegree.isEmpty()) {
        Class<? extends Startup> cls = zeroDegree.poll();
        Startup<?> startup = startupMap.get(cls);
        result.add(startup)
        //
        if (startupChildrenMap.containsKey(cls)) {
            List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
            for (Class<? extends Startup> childCls : childStartup) {
                Integer integer = inDegreeMap.get(childCls);
                inDegreeMap.put(cls, integer - 1);
                if (integer - 1 == 0) {
                    zeroDegree.offer(childCls);
                }
            }
        }
    }

    StartupSortStore startupSortStore = new StartupSortStore();
    startupSortStore.setResult(result);
    startupSortStore.setStartupMap(startupMap);
    startupSortStore.setStartupChildrenMap(startupChildrenMap);
    return startupSortStore;
}

任务的执行

任务排序排好之后,如何按顺序的执行这些任务呢?我们来定义两个任务执行管理类;

public class StartupCacheManager {

    /**
     * 执行结束的任务集合
     */
    private final ConcurrentHashMap<Class<? extends Startup>, Result> mInitializedComponents = new ConcurrentHashMap<>();
    /**
     * 单例
     */
    private static StartupCacheManager mInstance;

    /**
     * 私有构造
     */
    private StartupCacheManager() {}

    /**
     * 对外暴漏 StartupCacheManager
     * @return StartupCacheManager
     */
    public static StartupCacheManager getInstance() {
        if (mInstance == null) {
            synchronized (StartupCacheManager.class) {
                if (mInstance == null) {
                    mInstance = new StartupCacheManager();
                }
            }
        }
        return mInstance;
    }

    /**
     * save result of initialized component.
     * @param zClass zClass
     * @param result result
     */
    public void saveInitializedComponent(Class<? extends Startup> zClass, Result result) {
        mInitializedComponents.put(zClass, result);
    }

    /**
     * check initialized.
     * @param zClass zClass
     * @return true
     */
    public boolean hadInitialized(Class<? extends Startup> zClass) {
        return mInitializedComponents.containsKey(zClass);
    }

    /**
     * 获取 result
     * @param zClass zClass
     * @param <T> T
     * @return Result<T>
     */
    public <T> Result<T> obtainInitializedResult(Class<? extends Startup<T>> zClass) {
        return mInitializedComponents.get(zClass);
    }


    /**
     * 移除 result
     * @param zClass zClass
     */
    public void remove(Class<? extends Startup> zClass) {
        mInitializedComponents.remove(zClass);
    }

    /**
     * 清空 result
     */
    public void clear() {
        mInitializedComponents.clear();
    }
}

定义一个单例类 StartupCacheManager 用来缓存每一个任务的执行结果;例如,任务2 依赖任务1 的执行结果才能执行自己的任务,那么就需要缓存当前任务的执行结果;

public class StartupManager {

    /**
     * context
     */
    private final Context mContext;
    /**
     * 任务结合
     */
    private final List<AndroidStartup<?>> mStartupList;
    /**
     * 任务执行结果
     */
    private StartupSortStore mStartupSortStore;

    /**
     * 构造
     * @param mContext mContext
     * @param mStartupList mStartupList
     */
    public StartupManager(Context mContext, List<AndroidStartup<?>> mStartupList) {
        this.mContext = mContext;
        this.mStartupList = mStartupList;
    }

    /**
     * 启动任务,主线程调用
     * @return StartupManager
     */
    public StartupManager start() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new RuntimeException("please start task on main thread");
        }
        // 执行拓扑排序,核心
        mStartupSortStore = TopologySort.sort(mStartupList);
        for (Startup<?> startup : mStartupSortStore.getResult()) {
            Object result = startup.create(mContext);
            StartupCacheManager.getInstance().saveInitializedComponent(startup.getClass(), new Result(result));
        }
        return this;
    }

    public static class Builder {
        private final List<AndroidStartup<?>> startupList = new ArrayList<>();

        public Builder addTask(AndroidStartup<?> task) {
            startupList.add(task);
            return this;
        }

        public Builder addAllStartup(List<Startup<?>> startups) {
            startupList.addAll(startups);
            return this;
        }

        public StartupManager build(Context context) {
            return new StartupManager(context, startupList);
        }
    }

}

线程管理

我们要想完成任务的启动,本质上还是要合理的运用 CPU,那么就有必要把一些任务的启动放到子线程中并行执行;

例如:我们需要把前面定义的 5 个 Task 都要放到子线程中去执行,那么应该怎么实现?

又或者:我们在主线程中启动了 100 个子线程,这些子线程执行完毕之后,主线程才能打印完成,那么应该怎么实现呢?

我们可以借助 join、wait/notify、juc 工具包下的类等等;

但是,假设:A、B 两个线程,A 线程分别执行第一步、第二步、第三步,当 A 线程执行到第二步的时候,执行 B 线程,如何实现?

答案:join 不可以实现,使用 wait 和 notify,但是要 B 线程先 start;

假设:A、B、C 三个线程,A、B 线程执行三步,当 A、B 线程执行完第二步的时候,执行 C 线程,如何实现?

答案:A 、B 两个线程,在执行完第二步之后,调用 CountDownLatch 的 countdown 方法,C 线程调用CountDownLatch 的 await 方法等待被唤醒;

在这里插入图片描述

那么我们如何处理任务之间的同步?

在这里插入图片描述

那么 CountDowmLatch 如何应用到任务中呢?

public interface Dispatcher {

    /**
     * 当前任务是否在主线程执行
     * @return true 主线程调用
     */
    boolean callCreateOnMainThread();

    /**
     * 主线程是否需要等待该任务执行结束
     * @return true 等待主线程结束
     */
    boolean waitOnMainThread();

    /**
     * 等待
     */
    void toWait();

    /**
     * 唤醒
     */
    void toNotify();

    /**
     * 获取当前任务被执行的时候 需要的线程池
     * @return Executor
     */
    Executor executor();

    /**
     * 线程优先级
     * @return int
     */
    int getThreadPriority();

}

然后我们的 Startup 继承这个接口,这样我们的任务就默认是 Dispatch 的实现类了;callCreateOnMainThread 表示当前任务是否在主线程执行,false 则在子线程中执行;定义了 toWait 和 toNotify 方法,这两个方法我们进入 AndroidStartup 中看下:

public abstract class AndroidStartup<T> implements Startup<T> {

    /**
     * 执行线程调度,给每个任务都创建一个闭锁,对应的状态码是依赖的任务数
     *
     * 如果状态码是0,那么调用 await 的时候,并不会被阻塞
     */
    private final CountDownLatch mWaitDownLatch = new CountDownLatch(getDependenciesCount());
    
    
    /**
     * 唤醒
     */
    @Override
    public void toNotify() {
        mWaitDownLatch.countDown();
    }

    /**
     * 等待
     */
    @Override
    public void toWait() {
        try {
            mWaitDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

通过 CountDownLatch 来控制任务的 wait 和 notify;

然后,我们在任务启动的地方,改造如下:

public class StartupManager {
    
    
    /**
     * 启动任务,主线程调用
     * @return StartupManager
     */
    public StartupManager start() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new RuntimeException("please start task on main thread");
        }
        // 执行拓扑排序,核心
        mStartupSortStore = TopologySort.sort(mStartupList);
        for (Startup<?> startup : mStartupSortStore.getResult()) {
            StartupRunnable startupRunnable = new StartupRunnable(this, startup, mContext);
            // 定义了一个 StartupRunnable 将所有的任务都放到 runnable 中,如果是在主线程执行,则直接 run,否则加入到线程池中进行分发;
            if (startup.callCreateOnMainThread()) {
                startupRunnable.run();
            } else {
                startup.executor().execute(startupRunnable);
            }
        }
        return this;
    }
    
    
    /**
     * 通知依赖任务 notify
     * @param mStartup mStartup
     */
    public void notifyChildren(Startup<?> mStartup) {
        if (!mStartup.callCreateOnMainThread() && mStartup.waitOnMainThread()) {
            mCountDownLatch.countDown();
        }
        if (mStartupSortStore.getStartupChildrenMap().containsKey(mStartup.getClass())) {
            // 获取到依赖的任务
            List<Class<? extends Startup>> childStartupCls = mStartupSortStore.getStartupChildrenMap().get(mStartup.getClass());
            for (Class<? extends Startup> childStartupCl : childStartupCls) {
                // 通知子任务,父任务已经完成
                Startup<?> startup = mStartupSortStore.getStartupMap().get(childStartupCl);
                startup.toNotify();
            }
        }
    }
}

StartupRunnable 定义如下:

public class StartupRunnable implements Runnable {

    /**
     * StartupManager
     */
    private final StartupManager mStartupManager;
    /**
     * Startup
     */
    private final Startup<?> mStartup;
    /**
     * Context
     */
    private final Context mContext;

    /**
     * 构造任务启动器
     * @param mStartupManager mStartupManager
     * @param mStartup mStartup
     * @param mContext mContext
     */
    public StartupRunnable(StartupManager mStartupManager, Startup<?> mStartup, Context mContext) {
        this.mStartupManager = mStartupManager;
        this.mStartup = mStartup;
        this.mContext = mContext;
    }

    /**
     * 执行任务
     */
    @Override
    public void run() {
        Process.setThreadPriority(mStartup.getThreadPriority());
        // 所有任务执行 run 的时候都先 toWait 下,等待被唤醒,countDown 为 0 的则不会被 wait 而是直接执行;
        mStartup.toWait();
        Object result = mStartup.create(mContext);
        StartupCacheManager.getInstance().saveInitializedComponent(mStartup.getClass(), new Result(result));
        mStartupManager.notifyChildren(mStartup);
    }
}

如何处理主线程需要等待子线程执行完毕之后才能执行

如果任务5是在子线程,主线程需要等待任务5执行完成,主线程才能继续执行,应该怎么处理?
在这里插入图片描述
我们依然是借助于 juc 包下的工具类,StartupManager 中的 Builder 的 build 方法改造如下;

public StartupManager build(Context context) {
    AtomicInteger atomicInteger = new AtomicInteger();
    for (AndroidStartup<?> startup : startupList) {
        // 当前任务在线程调用,并且需要主线程等待其执行结束
        if (!startup.callCreateOnMainThread() && startup.waitOnMainThread()) {
            atomicInteger.decrementAndGet();
        }
    }
    CountDownLatch countDownLatch = new CountDownLatch(atomicInteger.get());
    return new StartupManager(context, startupList, countDownLatch);
}

这里我们就用到了 waitOnMainThread 方法,需要主线程等待当前任务执行完成才能继续往下执行;也就说这个方法只有在 callCreateOnMainThread 返回 false(也就是当前任务是在子线程执行)的时候,才有意义;

然后 StartupManager 中新增一个 await 方法;

/**
 * 等待任务 await
 */
public void await() {
    try {
        mCountDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

也就说,如果需要等待子线程任务初始化完成,那么就调用一下这个方法;

new StartupManager.Builder()
        .addStartup(CommonParamsTaskStartup)
        .addStartup(CuidTaskStartup)
        .addStartup(DeviceIdTaskStartup)
        .addStartup(ImTaskStartup)
        .addStartup(PushTaskStartup)
        .build(getContext())
        .start()
        .await();

然后 notifyChild 方法中,countDown 一下

public void notifyChildren(Startup<?> mStartup) {
    if (!mStartup.callCreateOnMainThread() && mStartup.waitOnMainThread()) {
        mCountDownLatch.countDown();
    }
    ....
}

大批量任务注册实现

当我们的程序中有很多很多的任务需要 addStartup 的时候,我们如果在 Application 中新增一个就要添加一个的话就比较繁琐,那么如何简化它的任务注册呢?能不能实现自动注册呢?

ContentProvider

这里我们可以借助 ContentProvider 来实现(思想来源于:LeakCanary),这也就是为什么 LeakCanary 不需要在 Application 中初始化依然可以使用的原因;

我们来增加一个 ContentProvider

public class StartupProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        List<Startup<?>> startups = StartupInitializer.discoverAndInitializer(this.getContext(), getClass().getName());
        new StartupManager.Builder()
                .addAllStartup(startups)
                .build(getContext())
                .start()
                .await();
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

以及 AndroidManifest 中进行注册(按需注册)

<provider
    android:authorities="${applicationId}.android_startup"
    android:name="com.example.startup.provider.StartupProvider"
    android:exported="false">
    <!-- 强制需要指定 meta-data 用来指定一个任务-->
    <meta-data
        android:name="com.example.startup.task.CommonParamsTaskStartup"
        android:value="android.startup" />
</provider>

然后解析 meta-data 获取指定的任务,然后就可以获取它依赖的任务,以及它依赖的任务依赖的其他任务;

public class StartupInitializer {

    public static String META_VALUE = "android.startup";

    public static List<Startup<?>> discoverAndInitializer(Context context,
                                                          String providerName) {
        try {
            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);
            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;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static void doInitialize(Startup<?> startup,
                                     Map<Class<? extends Startup>, Startup<?>> startups) throws Exception {
        //避免重复 不能使用List
        startups.put(startup.getClass(), startup);
        if (startup.getDependenciesCount() != 0) {
            //遍历父任务
            for (Class<? extends Startup<?>> dependency : startup.dependencies()) {
                doInitialize(dependency.newInstance(), startups);
            }
        }
    }

}

AndroidManifest 的 provider 中的 meta-data 必须指定最后执行的那个任务,这样才能向前找到所要依赖的任务;

拓扑优化

思考:同步任务阻塞异步任务怎么办?
在这里插入图片描述
我们来优化下排序算法:

public static StartupSortStore sort(List<? extends Startup<?>> startupList) {
    ```
    // 依次
    List<Startup<?>> result = new ArrayList<>();
    // 增加 主线程集合
    List<Startup<?>> main = new ArrayList<>();
    // 增加 子线程集合
    List<Startup<?>> threads = new ArrayList<>();
    // 如果 0 入度表不为空
    while (!zeroDegree.isEmpty()) {
        Class<? extends Startup> cls = zeroDegree.poll();
        Startup<?> startup = startupMap.get(cls);
        // 分别加入不到不同的集合中
        if (startup.callCreateOnMainThread()) {
            main.add(startup);
        } else {
            threads.add(startup);
        }
        //
        if (startupChildrenMap.containsKey(cls)) {
            List<Class<? extends Startup>> childStartup = startupChildrenMap.get(cls);
            for (Class<? extends Startup> childCls : childStartup) {
                Integer integer = inDegreeMap.get(childCls);
                inDegreeMap.put(cls, integer - 1);
                if (integer - 1 == 0) {
                    zeroDegree.offer(childCls);
                }
            }
        }
    }
    // 先添加子线程
    result.addAll(threads);
    // 再添加主线程
    result.addAll(main);

    StartupSortStore startupSortStore = new StartupSortStore();
    startupSortStore.setResult(result);
    startupSortStore.setStartupMap(startupMap);
    startupSortStore.setStartupChildrenMap(startupChildrenMap);
    return startupSortStore;
}

这样我们最终的排序就是 1、3、4、5 2

1 -> 子线程执行,直接执行
3 -> 子线程执行,等待 1 任务
4 -> 子线程执行,等待 2 任务
5 -> 子线程执行,等待 3、4 任务

2 -> 主线程执行,等待 1 任务

因为 result 先添加的 threads 任务,再添加的 main 任务

好了,启动优化就写到这里吧;

下一章预告


继续性能优化~

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值