App Startup 源码分析

抛砖引玉

使用过 LeakCanary 的童鞋都知道,早在 LeakCanary 1.x 版本的时候,我们需要在 Application 中手动调用 install 方法来完成 LeakCanary 的初始化过程。

LeakCanary 2.x 的版本实现了自动初始化。通过查看 LeakCanary 2.x 的源码,我们可以发现,它通过继承 ContentProvider ,在 onCreate() 中来完成初始化过程。

internal sealed class AppWatcherInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application

    val leakCanaryEnable = application
            .getSharedPreferences(KEY_LEAK_CANARY_ENABLE_FILE, Context.MODE_PRIVATE)
            .getBoolean(KEY_LEAK_CANARY_ENABLE, false)
    if (!leakCanaryEnable) {
      Log.d("LeakCanary", "Sorry ! you can't init LeakCanary")
      return true
    }
    InternalAppWatcher.install(application)
    return true
  }
  
}

为什么使用 ContentProvider 就能够达到自动初始化呢?

在应用程序启动过程中,会调用到 ActivityThread.handleBindApplication(),我们可以看到如下关键代码。

// 1. 这里会反射创建 Application, 并调用 Application.attach()
app = data.info.makeApplication(data.restrictedBackupMode, null);

if (!data.restrictedBackupMode) {
    if (!ArrayUtils.isEmpty(data.providers)) {
        // 2. 这里会装载 ContentProvider
        installContentProviders(app, data.providers);
    }
}

try {
    // 3. 这里会调用Application 的 onCreate()
    mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
}

通过源码,我们知道它的调用顺序如下。

  1. 创建 Application,调用 Application.attach()
  2. 装载 ContentProvider
  3. 调用 Application.onCreate();

所以在 ContentProvider.onCreate() 里面是可以拿到 application context , 在这里进行初始化是没有问题的。

而 本文介绍的 Jetpack 的新成员 App Startup 就是通过 ContentProvider 来完成实现的。

App Startup 的使用

使用 App Startup 库,我们需要在 build.gradle 中添加依赖。

implementation "androidx.startup:startup-runtime:1.0.0-alpha01"

实现 Initializer 接口,为组件创建初始化程序。

举个栗子,这里为 LibraryA 创建初始化程序,dependencies() 返回了一个集合,表明它依赖于其他库的初始化。

class LibraryAInitializer : Initializer<LibraryA> {

    override fun create(context: Context): LibraryA {
        return LibraryA.getInstance(context)
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return arrayListOf()
    }

}

manifest.xml 文件中配置 InitializationProvider, App Startup 使用指定的 ContentProvider 来检索和初始化 Initializer

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

每个 Initializer 使用 <meta-data /> 节点定义,其 value 必须为 androidx.startup(这里后面源码会讲到)。

<meta-data
    android:name="com.ppdai.code.jetpack.startup.initializer.LibraryAInitializer"
    android:value="androidx.startup" />

这样,在 app 启动过程中就会自动初始化 LibraryA 这个库,是不是非常的简单。

假设 LibraryA 的初始化依赖于 LibraryB,我们只需要编写 LibraryB 的 Initializer,然后修改 LibraryAInitializer,在其 dependencies() 方法中返回 LibraryB 的 Initializer 即可。

class LibraryAInitializer : Initializer<LibraryA> {

    override fun create(context: Context): LibraryA {
        return LibraryA.getInstance(context)
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return arrayListOf(LibraryBInitializer::class.java)
    }

}

class LibraryBInitializer : Initializer<LibraryB> {

    override fun create(context: Context): LibraryB {
        return LibraryB.getInstance(context)
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return arrayListOf()
    }

}

这样在初始化 LibraryA 的时候会先初始化 LibraryB。

如果不希望在 app 启动的时候初始化,我们也可以通过 AppInitializer.initializeComponent() 在代码中对指定的组件进行初始化,如下所示。

AppInitializer.getInstance(this).initializeComponent(LibraryAInitializer::class.java)

源码分析

InitializationProvider

InitializationProviderApplication.onCreate() 调用之前检索和初始化 Initializer

我们来看看 InitializationProvider 的具体实现,可以发现它只是在 onCreate() 方法中调用了 AppInitializerdiscoverAndInitialize()

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;
    }

    @Nullable
    @Override
    public Cursor query(
            @NonNull Uri uri,
            @Nullable String[] projection,
            @Nullable String selection,
            @Nullable String[] selectionArgs,
            @Nullable String sortOrder) {
        throw new IllegalStateException("Not allowed.");
    }

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

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int delete(
            @NonNull Uri uri,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int update(
            @NonNull Uri uri,
            @Nullable ContentValues values,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        throw new IllegalStateException("Not allowed.");
    }
}

AppInitializer

AppInitializer 是真正对 Initializer 进行初始化的地方。

AppInitializer 可以拆分为三个部分。

  1. AppInitializer 的创建;
  2. 如何初始化在 manifest.xml 中配置的 Initializer
  3. 如何在代码中初始化指定 Initializer

创建

AppInitializer 使用单例模式。成员变量 mInitialized 用来存放已经初始化的 Initializer,保证所有的 Intializer 只被初始化一次。

private static AppInitializer sInstance;

// 用来存放已初始化的 Initializer
@NonNull
final Map<Class<?>, Object> mInitialized;

@NonNull
final Context mContext;

AppInitializer(@NonNull Context context) {
    mContext = context.getApplicationContext();
    mInitialized = new HashMap<>();
}

@NonNull
@SuppressWarnings("UnusedReturnValue")
public static AppInitializer getInstance(@NonNull Context context) {
    synchronized (sLock) {
        if (sInstance == null) {
            sInstance = new AppInitializer(context);
        }
        return sInstance;
    }
}

初始化 manifest.xml 中配置的 Initializer

这个过程主要是通过 discoverAndInitialize 这个方法来完成的

void discoverAndInitialize() {
    try {
        Trace.beginSection(SECTION_NAME);
        // 1. 获取InitializationProvider在manifest中配置的meta-data
        ComponentName provider = new ComponentName(mContext.getPackageName(),
                InitializationProvider.class.getName());
        ProviderInfo providerInfo = mContext.getPackageManager()
                .getProviderInfo(provider, GET_META_DATA);
        Bundle metadata = providerInfo.metaData;
        // androidx.startup
        String startup = mContext.getString(R.string.androidx_startup);
        if (metadata != null) {
            Set<Class<?>> initializing = new HashSet<>();
            Set<String> keys = metadata.keySet();
            // 2. 遍历meta-data,找到所有value="androidx.startup"的元素,进行初始化
            for (String key : keys) {
                String value = metadata.getString(key, null);
                if (startup.equals(value)) {
                    Class<?> clazz = Class.forName(key);
                    if (Initializer.class.isAssignableFrom(clazz)) {
                        Class<? extends Initializer<?>> component =
                                (Class<? extends Initializer<?>>) clazz;
                        if (StartupLogger.DEBUG) {
                            StartupLogger.i(String.format("Discovered %s", key));
                        }
                        // 3. 初始化指定的 Initializer
                        doInitialize(component, initializing);
                    }
                }
            }
        }
    } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
        throw new StartupException(exception);
    } finally {
        Trace.endSection();
    }
}

可以看出,通过解析 meta-data ,我们就能拿到配置的 Initializer,然后逐一调用 doInitialize() 进行初始化。

再看看 doInitialize 做了什么事情。

<T> T doInitialize(
        @NonNull Class<? extends Initializer<?>> component,
        @NonNull Set<Class<?>> initializing) {
    synchronized (sLock) {
        boolean isTracingEnabled = Trace.isEnabled();
        try {
            if (isTracingEnabled) {
                // Use the simpleName here because section names would get too big otherwise.
                Trace.beginSection(component.getSimpleName());
            }
            if (initializing.contains(component)) {
                String message = String.format(
                        "Cannot initialize %s. Cycle detected.", component.getName()
                );
                throw new IllegalStateException(message);
            }
            Object result;
            // 1. 保证 Initializer 只初始化一次
            if (!mInitialized.containsKey(component)) {
                initializing.add(component);
                try {
                    // 2. 通过反射创建 Initializer 
                    Object instance = component.getDeclaredConstructor().newInstance();
                    Initializer<?> initializer = (Initializer<?>) instance;
                    // 3. 获取 Initializer 的依赖关系
                    List<Class<? extends Initializer<?>>> dependencies =
                            initializer.dependencies();

                    // 4. 递归初始化,先初始化 Initializer 依赖的且没有被初始化过的 Initializer
                    if (!dependencies.isEmpty()) {
                        for (Class<? extends Initializer<?>> clazz : dependencies) {
                            if (!mInitialized.containsKey(clazz)) {
                                doInitialize(clazz, initializing);
                            }
                        }
                    }
                    if (StartupLogger.DEBUG) {
                        StartupLogger.i(String.format("Initializing %s", component.getName()));
                    }
                    // 5. 初始化自身
                    result = initializer.create(mContext);
                    if (StartupLogger.DEBUG) {
                        StartupLogger.i(String.format("Initialized %s", component.getName()));
                    }
                    initializing.remove(component);
                    // 6. 将 Initializer 放入已初始化 Map
                    mInitialized.put(component, result);
                } catch (Throwable throwable) {
                    throw new StartupException(throwable);
                }
            } else {
                result = mInitialized.get(component);
            }
            return (T) result;
        } finally {
            Trace.endSection();
        }
    }
}

在代码中初始化指定的Initializer

我们可以通过 AppInitializerinitializeComponent() 在代码中初始化指定的 Initializer

public <T> T initializeComponent(@NonNull Class<? extends Initializer<T>> component) {
    return doInitialize(component, new HashSet<Class<?>>());
}

可以看到,它实际上也是调用了 doInitialize() 方法。

Initializer

用来在 app startup 过程中初始化 libraries

public interface Initializer<T> {

    // application context
    @NonNull
    T create(@NonNull Context context);

    // 返回 Initializer 的依赖。比如初始化 A,A 依赖于B,那么在初始化 A 的时候会优先查看 B 是否初始化了,如果没有初始化 B,则会先初始化 B。
    @NonNull
    List<Class<? extends Initializer<?>>> dependencies();
}

参考

App Startup

Github/Jetpack-App Startup

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容