4

食之无味?App Startup 可能比你想象中要简单 - 彭旭锐

 1 year ago
source link: https://www.cnblogs.com/pengxurui/p/16656217.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

食之无味?App Startup 可能比你想象中要简单

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

大家好,我是小彭。

过去两年,我们在掘金平台上发布 JetPack 专栏文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。

2020 年 10 月 28 日,JetPack | App Startup 1.0.0 终于迎来正式发布,正好最近在总结组件化架构专题,所以也专门学习下 App Startup 的工作原理。在这篇文章里,我将带你总结 App Startup 的使用方法 & 实现原理 & 源码分析。有用请点赞给 Star,给小彭一点创作的动力,谢谢。


这篇文章是 Jetpack 系列文章第 13 篇,专栏文章列表:

二、其他:

  • 1、AppStartup:轻量级初始化框架(本文)
  • 2、DataStore:新一代键值对存储方案
  • 3、Room:ORM 数据库访问框架
  • 4、WindowManager:加强对多窗口模式的支持
  • 5、WorkManager:加强对后台任务的支持
  • 6、Compose:新一代视图开发方案

学习路线图:


1. 认识 AppStartup

1.1 App Startup 解决了什么问题?

App Startup 是 Google 提供的 Android 轻量级初始化框架:

  • 优点:使用 App Startup 框架,可以简化启动序列并显式设置初始化依赖顺序,在简单、高效这方面,App Startup 基本满足需求。
  • 不足:App Startup 框架的不足也是因为它太简单了,提供的特性太过简单,往往并不能完美契合商业化需求。例如以下特性 App Startup 就无法满足:
    • 缺乏异步等待: 同步等待指的是在当前线程先初始化所依赖的组件,再初始化当前组件,App Startup 是支持的,但是异步等待就不支持了。举个例子,所依赖的组件需要执行一个耗时的异步任务才能完成初始化,那么 App Startup 就无法等待异步任务返回;
    • 缺乏依赖回调: 当前组件所依赖的组件初始化完成后,未发出回调。

1.2 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 在应用启动的时候初始化的特性,提供了一个自定义 ContentProvider 来实现自动初始化。很多库都利用了 ContentProvider 的启动机制,来实现无侵入初始化,例如 LeakCanary 等

使用 AppStartup 还能够合并所有用于初始化的 ContentProvider ,减少创建 ContentProvider,并提供全局管理。

App Startup 示意图

详细的源码分析下文内容。


2. App Startup 使用方法

这一节,我们来总结 App Startup 的使用步骤。

2.1 基本用法

  • 1、添加依赖

在模块级 build.gradle 添加依赖:

模块级 build.gradle

implementation "androidx.startup:startup-runtime:1.0.0"
  • 2、实现 Initializer 接口

Initializer 接口是 App Startup 定义组件接口,用于指定组件的初始化逻辑和初始化顺序(也就是依赖关系),接口定义如下:

  • 1、create(...) 初始化操作: 返回的初始化结果将被缓存,其中 context 参数就是当前进程的 Application 对象;
  • 2、dependencies() 依赖关系: 返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。

Initializer.java

public interface Initializer<T> {

    // 1、初始化操作,返回值将被缓存??
    @NonNull
    T create(@NonNull Context context);

    // 2、依赖关系,返回值是一个依赖组件的列表
    @NonNull
    List<Class<? extends Initializer<?>>> dependencies();
}

示例程序

// LeakCanary 2.9.1
internal class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> {
    override fun create(context: Context) = apply {
        // 实现初始化操作
        val application = context.applicationContext as Application
            AppWatcher.manualInstall(application)
        }
	
    override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
  • 3、配置

在 Manifest 文件中将 Initializer 实现类配置到 androidx.startup.InitializationProvider<meta-data> 中。

示例程序

<!-- LeakCanary 2.9.1 -->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">

    <meta-data
        android:name="leakcanary.internal.AppWatcherStartupInitializer"
        android:value="androidx.startup"/>
</provider>

要点如下:

  • 1、组件名必须是 androidx.startup.InitializationProvider
  • 2、需要声明 android:exported="false",以限制其他应用访问此组件;
  • 3、要求 android:authorities 要求在设备中全局唯一,通常使用 ${applicationId} 作为前缀;
  • 4、需要声明 tools:node="merge",确保 manifest merger tool 能够正确解析冲突的节点;
  • 5、meta-data android:name 为组件的 Initializer 实现类的全限定类名,android:value 固定为 androidx.startup

提示: 为什么要将 androidx.startup 设置为 value,而不是 name?因为在键值对中,name 是唯一的,而 value 是允许重复的,将 androidx.startup 放到 value 的话才能允许同时配置多个相同语义的 <meta-data>

至此,App Startup 基本的使用与配置完成,在应用启动时,App Startup 会自动收集各个模块配置的 Initializer 实现类,并按照依赖顺序依次执行。

2.2 进阶用法

  • 1、手动初始化

当你的组件需要进行手动初始化,而不是自动初始化时(例如存在耗时任务),可以进行手动初始化,而且手动初始化是可以在子线程调用的,而自动初始化均是在主线程执行的。

  • App Startup 中会缓存初始化后的结果,重复调用 initializeComponent() 也不会导致重复初始化;
  • 要手动初始化的 Initializer 实现类不能在声明到 AndroidManifest 中,也不能被其它组件依赖,否则它依然会自动初始化。

调用以下方即可进行手动初始化:

示例程序

AppInitializer.getInstance(context).initializeComponent(ExampleLoggerInitializer::class.java)
  • 2、取消自动初始化

假如有些库已经配置了自动初始化,而我们又希望进行懒加载时,就需要利用 manifest merger tool 的合并规则来移除这个库对应的 Initializer。具体如下:

示例程序

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.example.ExampleLoggerInitializer"
        tools:node="remove" />
</provider>
  • 3、禁用 App Startup

假如需要完全禁用 App Startup 自动初始化,同样也可以利用到 manifest merger tool 的合并规则:

示例程序

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />

3. App Startup 原理分析

3.1 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 的启动机制实现自动初始化。ContentProvider 通常的用法是为当前进程 / 远程进程提供内容服务,它们会在应用启动的时候初始化。利用这个特性,App Startup 的方案就是自定义一个 ContentProvider 的实现类 InitializationProvider,在 onCreate(…) 方法中执行初始化逻辑。

InitializationProvider.java

已简化

public final class InitializationProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        Context context = getContext();
        if (context != null) {
            // 初始化
            AppInitializer.getInstance(context).discoverAndInitialize();
        } else {
            throw new StartupException("Context cannot be null");
        }
        return true;
    }

    @Override
    public Cursor query(...) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public String getType(...) {
        throw new IllegalStateException("Not allowed.");
    }

    @Nullable
    @Override
    public Uri insert(...) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int delete(...) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int update(...) {
        throw new IllegalStateException("Not allowed.");
    }
}

由于 ContentProvider 的其他方法是没有意义的,所以都抛出了 IllegalStateException

3.2 说一下 App Startup 的初始化过程

从上一节可以看到,App Startup 在 InitializationProvider 中调用了AppInitializer#discoverAndInitialize()执行自动初始化。AppInitializer是 App StartUp 框架的核心类,整个 App Startup 框架的代码其实非常少,其中很大部分核心代码都在 AppInitializer 类中。

我将整个自动初始化过程概括为 3 个阶段:

  • 步骤 1 - 获取 数据: 扫描 Manifest 中定义在 InitializationProvider 里面的 数据,从中筛选出 Initializer 的配置信息;
  • 步骤 2 - 递归执行初始化器: 通过 Initializer#create() 执行每个初始化器的逻辑,并且会通过 Initializer#dependencies() 优先保证依赖项已经初始化;
  • 步骤 3 - 缓存初始化结果: 将初始化后的结果缓存到映射表中,避免重复初始化。

源码摘要如下:

AppInitializer.java

private static final Object sLock = new Object(); // 后面会提到

// 记录扫描 <meta-data> 得到的初始化器(可用于判断组件是否已经自动启动)
final Set<Class<? extends Initializer<?>>> mDiscovered;

// 缓存每个组件的初始化结果
final Map<Class<?>, Object> mInitialized;

void discoverAndInitialize() {
    // 1、获取 androidx.startup.InitializationProvider 组件信息
    ComponentName provider = new ComponentName(mContext.getPackageName(), InitializationProvider.class.getName());
    ProviderInfo providerInfo = mContext.getPackageManager().getProviderInfo(provider, GET_META_DATA);

    // 2、androidx.startup 字符串
    String startup = mContext.getString(R.string.androidx_startup);

    // 3、获取组件信息中的 meta-data 数据
    Bundle metadata = providerInfo.metaData;

    // 4、遍历所有 meta-data 数据
    if (metadata != null) {
        Set<Class<?>> initializing = new HashSet<>();
        Set<String> keys = metadata.keySet();
        for (String key : keys) {
            String value = metadata.getString(key, null);

            // 4.1 筛选 value 为 androidx.startup 的 meta-data 数据中
            if (startup.equals(value)) {
                Class<?> clazz = Class.forName(key);

                // 4.2 检查指定的类是 Initializer 接口的实现类
                if (Initializer.class.isAssignableFrom(clazz)) {
                    Class<? extends Initializer<?>> component = (Class<? extends Initializer<?>>) clazz;

                    // 4.3 将 Class 添加到 mDiscovered Set 中
                    mDiscovered.add(component);

                    // 4.4 初始化此组件
                    doInitialize(component, initializing);
                }
            }
        }
    }
}

// -> 4.3 mDiscovered 用于判断组件是否已经自动启动
public boolean isEagerlyInitialized(@NonNull Class<? extends Initializer<?>> component) {
    return mDiscovered.contains(component);
}

// -> 4.4 初始化此组件(已简化)
<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
    // 1、对 sLock 加锁,我后文再说。

    Object result;

    // 2、判断 initializing 中存在当前组件,说明存在循环依赖
    if (initializing.contains(component)) {
        String message = String.format("Cannot initialize %s. Cycle detected.", component.getName());
        throw new IllegalStateException(message);
    }

    // 3、检查当前组件是否已初始化
    if (!mInitialized.containsKey(component)) {
        // 3.1 当前组件未初始化

        // 3.1.1 记录正在初始化
        initializing.add(component);

        // 3.1.2 通过反射实例化 Initializer 接口实现类
        Object instance = component.getDeclaredConstructor().newInstance();
        Initializer<?> initializer = (Initializer<?>) instance;

        // 3.1.3 遍历所依赖的组件(关键:优先处理依赖的组件)
        List<Class<? extends Initializer<?>>> dependencies = initializer.dependencies();
        if (!dependencies.isEmpty()) {
            for (Class<? extends Initializer<?>> clazz : dependencies) {

                // 递归:如果所依赖的组件未初始化,执行初始化
                if (!mInitialized.containsKey(clazz)) {
                    // 注意:这里将 initializing 作为参数传入,用于判断循环依赖
                    doInitialize(clazz, initializing); 
                }
            }
        }

        // 3.1.4 (到这里,所依赖的组件已经初始化完成)初始化当前组件
        result = initializer.create(mContext);

        // 3.1.5 移除正在初始化记录
        initializing.remove(component);

        // 3.1.6 缓存初始化结果
        mInitialized.put(component, result);
    } else {
        // 3.2 当前组件已经初始化,直接返回
        result = mInitialized.get(component);
    }
     return (T) result;
}

3.3 手动初始化的执行过程

前面我们提到使用 initializeComponent() 方法可以手动初始化,我们来看手动初始化(懒加载)的源码:

AppInitializer.java

public <T> T initializeComponent(@NonNull Class<? extends Initializer<T>> component) {
    // 调用 doInitialize(...) 方法:
    return doInitialize(component, new HashSet<Class<?>>());
}

其实非常简单,就是调用上一节的 doInitialize(...) 执行初始化。需要注意的是,这个方法是允许在子线程调用的,换句话说,自动初始化与手动初始化是存在线程同步问题的,那么 App Startup 是如何解决的呢?还记得我们前面有一个 sLock 没有说吗?其实它就是用来保证线程同步的锁:

AppInitializer.java

<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
    // 1、对 sLock 加锁
    synchronized (sLock) {
        ...
    }
}

到这里,App Startup 的内容就讲完了。可以看到 App Startup 只是一个轻量级的初始化框架,能做的事情有限。市面上有开发者开源了基于 DAU 有向无环图的初始化框架,这个我们下次再说。关注我,带你了解更多。


我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK