Android 组件初始化三种方式

方式一:使用 Application#onCreate 进行初始化

使用方式

  1. 自定义 CustomApplication
class CustomApplication : Application() {
    // ..
    override fun onCreate() {
        super.onCreate()
        // 进行组件初始化
    }
    // ..
}

  1. 主module 清单文件中声明使用 CustomApplication
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- ... -->
    <application android:name="CustomApplication全路径或者相对于资源的路径">
        <!-- ... -->
    </application>
</manifest>

优缺点

  • 优点:简单易用,只需在 主moduleApplication#onCreate 进行组件初始化,并且可以指定组件间初始化顺序;
  • 缺点:在其他 主module 使用的时候,需要在自身 Application#onCreate 进行一遍初始化(对应组件依赖方来说,较为繁琐);同时如果组件存在依赖关系,使用方要清楚组件之间的关系,从而确定组件初始化顺序,增加组件使用成本;

方式二:使用 Content Provider 进行初始化

使用方式

  1. 自定义 ContentProvider
class CustomProvider : ContentProvider() {
    
    override fun onCreate(): Boolean {
        // 初始化组件
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? = null

    override fun getType(uri: Uri): String? = null

    override fun insert(uri: Uri, values: ContentValues?): Uri? = null

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?
    ): Int = 0
}

  1. 自定义 ContentProvider 在当前组件 AndroidManifest.xml 进行声明
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- ... -->
    <application>
        <!-- 
             1. name 为 CustomProvider 的全路径,或者相对于资源文件的路径 
             2. authorities 加上 ${applicationId} 可以有效避免多个应用依赖该组件重复,导致不能安装问题
             3. exported = false,让外部不能使用,纯粹只是为了当前组件的初始化
        -->
        <provider
            android:name="CustomProvider的路径"
            android:authorities="${applicationId}.custom-startup"
            android:exported="false" />
        <!-- ... -->
    </application>
</manifest>

优缺点

  • 优点:在应用启动的时候,系统会调用 ContentProvider#onCreate 进行初始化,则避免组件使用方主moduleApplication#onCreate 初始化,对于组件使用方来说是无感知的
  • 缺点:如果组件间初始化是有依赖性,则多个 Provider 在清单文件中的顺序是有要求的,但这个是非常困难进行调整的,并且很容易错;同时 Provider实例化成本高昂,在不必要的情况下可能会拖慢启动序列

ContentProvider#onCreate 初始化路径(基于android33)

  • ActivityThread#main(zygote 新建进程,执行 ActivityThreadmain 方法,执行主 Looper 循环)
  • ActivityThread#attach(ActivityThread 初始化)
  • ActivityManagerService#attachApplication(Binder 调用,从应用 => ActivityManagerService)
  • ApplicationThread#bindApplication(Binder 调用,从 ActivityManagerService => 应用)
  • H#handleMessage(BIND_APPLICATION)(使用 H 进行分发)
  • ActivityThread#handleBindApplication(当前应用绑定 application)
  • ActivityThread#installContentProviders(安装多个 Provider)
  • ActivityThread#installProvider(安装单个 Provider)
  • ContentProvider#attachInfo
  • ContentProvider#onCreate

方式三:使用 Jetpack startup 组件库进行初始化

使用方式

  1. module 模块的 build.gradle 引用 Jetpack startup
dependencies {
    implementation "androidx.startup:startup-runtime:1.1.1"
}

  1. 继承 Initializer<*>,重写 onCreatedependencies 方法,
class CustomInitializer : Initializer<Custom> {
    
    override fun create(context: Context): Custom {
        // Custom 初始化
        return Custom.getInstance(context)
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 声明当前依赖的类库
        return emptyList()
    }
}

  1. 在模块 AndroidManifest.xml 中声明
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- ... -->
    <application>
        <!-- 
             1. name 为固定 androidx.startup.InitializationProvider,则知道是用这个来进行初始化的
             2. authorities = "${applicationId}.androidx-startup" 用于避免多个应用导致的重复
             3. exported 仅供自身使用,不对外暴露
             4. tools:node = "merge" 表示合并相同的 InitializationProvider,在这里代表合并其他组件的 meta-data
-->
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <!-- 
                 1.  name 为 CustomInitializer 的全路径,或者相对于资源文件的路径
                 2.  value 为固定的 androidx.startup,用于提取 CustomInitializer 的路径
-->
            <meta-data  android:name="CustomInitializer的路径"
                  android:value="androidx.startup" />
        </provider>
        <!-- ... -->
    </application>
    
</manifest>

优缺点

  • 优点:解决方法二中的2个问题;在整体应用中,只存在一个 Provider,并且 Initializer#dependencies 中声明该组件所依赖组件,在初始该组件的时候会先初始化该组件所依赖组件
  • 缺点:从 Initializer#dependencies 方法参数可知,需要所依赖组件实现 Initializer 才可以,需要改动原来的组件;但如果后续的组件都按照此来声明,对于使用方来说,会更加简单

源码解析

  1. android:name 可知 InitializationProvider 入口
// androidx.startup.InitializationProvider
public class InitializationProvider extends ContentProvider {

    @Override
    public final boolean onCreate() {
        Context context = getContext();
        if (context != null) {
            Context applicationContext = context.getApplicationContext();
            if (applicationContext != null) {
                // 调用 AppInitializer 的 discoverAndInitialize
                AppInitializer.getInstance(context).discoverAndInitialize();
            } else {
                StartupLogger.w("Deferring initialization because `applicationContext` is null.");
            }
        } else {
            throw new StartupException("Context cannot be null");
        }
        return true;
    }

    // ...
}

  1. AppInitializer#discoverAndInitialize
public final class AppInitializer {

    private static volatile AppInitializer sInstance;
    private static final Object sLock = new Object();
    final Map<Class<?>, Object> mInitialized;  
    final Set<Class<? extends Initializer<?>>> mDiscovered;
    final Context mContext; 

    @NonNull
    public static AppInitializer getInstance(@NonNull Context context) {
        // 双重校验获取 AppInitializer
        if (sInstance == null) {
            synchronized (sLock) {
                if (sInstance == null) {
                    sInstance = new AppInitializer(context);
                }
            }
        }
        return sInstance;
    }

    AppInitializer(@NonNull Context context) {
        mContext = context.getApplicationContext(); // application context
        mDiscovered = new HashSet<>();  // 已发现的Initializer
        mInitialized = new HashMap<>(); // 已初始化的内容
    }
    
    void discoverAndInitialize() {
        try {
            // step1: 创建包含 InitializationProvider 类信息的 ComponentName
            ComponentName provider = new ComponentName(mContext.getPackageName(),
                    InitializationProvider.class.getName());
            // step2: 获取 InitializationProvider 对应的 META_DATA 信息
            ProviderInfo providerInfo = mContext.getPackageManager()
                    .getProviderInfo(provider, GET_META_DATA);
            Bundle metadata = providerInfo.metaData;
            // step3: 解析metadata
            discoverAndInitialize(metadata);
        } catch (PackageManager.NameNotFoundException exception) {
            throw new StartupException(exception);
        }
    }

    void discoverAndInitialize(@Nullable Bundle metadata) {
        // step4: 获取 startup 字符串
        String startup = mContext.getString(R.string.androidx_startup);
        try {
            if (metadata != null) {
                // step5: initializing 记录正在初始化的类,主要用于防止循环引用
                Set<Class<?>> initializing = new HashSet<>();
                Set<String> keys = metadata.keySet();
                for (String key : keys) {
                    String value = metadata.getString(key, null);
                    // step6: 这里限定了 android:value 只能为 startup 的值
                    if (startup.equals(value)) {
                        // step7: 反射获取此类,并检查是否是 Initializer 的子类
                        Class<?> clazz = Class.forName(key);
                        if (Initializer.class.isAssignableFrom(clazz)) {
                            Class<? extends Initializer<?>> component =
                                    (Class<? extends Initializer<?>>) clazz;
                            // step8: 记录当前类进入发现集合中
                            mDiscovered.add(component);
                        }
                    }
                }
                // step9: 遍历当前发现的类,开始初始化
                for (Class<? extends Initializer<?>> component : mDiscovered) {
                    // Tips: initializing 记录正在初始化的类,这里为空集合
                    doInitialize(component, initializing);
                }
            }
        } catch (ClassNotFoundException exception) {
            throw new StartupException(exception);
        }
    }
    
    @NonNull
    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
    private <T> T doInitialize(
            @NonNull Class<? extends Initializer<?>> component,
            @NonNull Set<Class<?>> initializing) {
        try {
            // step10: 如果 initializing 包含 component,则证明出现循环依赖
            if (initializing.contains(component)) {
                String message = String.format(
                        "Cannot initialize %s. Cycle detected.", component.getName()
                );
                throw new IllegalStateException(message);
            }
            
            Object result;
            // step11: 检查此类是否已初始化,已初始化这直接返回,反之进行初始化
            if (!mInitialized.containsKey(component)) {
                initializing.add(component);
                
                try {
                    // step12: 使用反射调用构造参数;由这可知这里需要一个默认的空参的构造函数
                    Object instance = component.getDeclaredConstructor().newInstance();
                    Initializer<?> initializer = (Initializer<?>) instance;
                    List<Class<? extends Initializer<?>>> dependencies =
                            initializer.dependencies();
                    
                    if (!dependencies.isEmpty()) {
                        for (Class<? extends Initializer<?>> clazz : dependencies) {
                            // step13: 如果当前依赖组件未进行初始化,则进行初始化
                            if (!mInitialized.containsKey(clazz)) {
                                doInitialize(clazz, initializing);
                            }
                        }
                    }
                    
                    // step14: 调用 Initializer#create 创建组件
                    result = initializer.create(mContext);
                    // step15: 当前组件初始化完成,从正在初始化集合移除和加入已初始化集合中
                    initializing.remove(component);
                    mInitialized.put(component, result);
                } catch (Throwable throwable) {
                    throw new StartupException(throwable);
                }
            } else {
                result = mInitialized.get(component);
            }
            return (T) result;
        }
    }
    
    // ======================= 剩余方法 =======================

    @NonNull
    @SuppressWarnings("unused")
    public <T> T initializeComponent(@NonNull Class<? extends Initializer<T>> component) {
        return doInitialize(component);
    }


    public boolean isEagerlyInitialized(@NonNull Class<? extends Initializer<?>> component) {
        return mDiscovered.contains(component);
    }

    @NonNull
    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
    <T> T doInitialize(@NonNull Class<? extends Initializer<?>> component) {
        Object result;
        synchronized (sLock) {
            result = mInitialized.get(component);
            if (result == null) {
                result = doInitialize(component, new HashSet<Class<?>>());
            }
        }
        return (T) result;
    }    
}

Tips

从上述的源码解析可知,整个过程分为2部分

  1. 提取当前 Providermeta-data 数据,从 meta-data 上可以获取对应 Initializer 的类信息
  2. 根据提取到 Initializer 的类信息,进行反射调用构造函数和调用 onCreate 方法;

因此可以不使用第一步进行初始化,选择合适时机进行初始化,也就官网说的延迟初始化, 此时调用上述 AppInitializer#initializeComponent 进行初始化

默认行为是从 Provider 清单文件声明 meta-data 提取类信息,因此当不需要某个初始化的时候,可以屏蔽对应 meta-data 的类信息;或者使用 aapt2 所带的 tools:node="remove" 进行移除

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- tools:node="remove" 在构建的时候,不会打进去 -->
    <meta-data android:name="com.example.ExampleLoggerInitializer"
              tools:node="remove" />
</provider>

同时 tools:node="remove" 也适用于 provider 结点,使整个 provider 结点移除

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

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

推荐阅读更多精彩内容