Jetpack家族新成员,App Startup学习笔记

近日,南昌市人民政府与新电商平台拼多多签订战略框架协议,共同启动“南昌优品”电商直播消费节、“南昌优品馆”线上大型展销专场等系列活动。

/ 解决的问题 /

一般需要初始化的sdk都会对外提供一个初始化方法供外界调用,如:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Sdk1.init(this);
    }
}

对调用者很不友好。另一种做法是使用ContentProvider初始化,如下:

public class Sdk1InitializeProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        Sdk1.init(getContext());
        return true;
    }
    ...
}

然后在AndroidManifest.xml文件中注册这个privoder,如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.zhengzhengxiaogege.sdk1">
    <application>
        <provider
            android:authorities="${applicationId}.init-provider"
            android:name=".Sdk1InitializeProvider"
            android:exported="false"/>
    </application>
</manifest>

这样初始化的逻辑就由Sdk开发者在内部完成了。

但是,如果一个app依赖了很多需要初始化的sdk,如果都放在一个ContentProvider中会导致此ContentProvider代码数量增加。而且每增加一个需要初始化的sdk都要对该ContentProvider文件做改动,不方便合作开发。而如果每个sdk都采用同样的方式将会带来性能问题。App Startup library可以有效解决这个问题。

/ 使用App Startup /

添加依赖

在App模块的build.gradle文件中添加依赖:

dependencies {
    implementation "androidx.startup:startup-runtime:1.0.0-alpha01"
}

实现Initializer<T>接口

app通过Initializer<T>接口接入App Startup,需要实现两个方法

public interface Initializer<T> {

  @NonNull
  T create(@NonNull Context context);

  @NonNull
  List<Class<? extends Initializer<?>>> dependencies();
}

例如有一个Sdk1如下:

public class Sdk1 {

  private static final String TAG = "Sdk1";

  private static Context sApplicationContext;

  private static volatile Sdk1 sInstance;

  public static void init(Context applicationContext){
      sApplicationContext = applicationContext;
      Log.e(TAG, "Sdk1 is initialized");
  }

  public static Sdk1 getInstance(){
      if (sInstance == null) {
          synchronized (Sdk1.class){
              if (sInstance == null) {
                  sInstance = new Sdk1();
              }
          }
      }
      return sInstance;
  }

  private Sdk1(){
  }

  public void printApplicationName(){
      Log.e(TAG, sApplicationContext.getPackageName());
  }
}

sdk1对外提供Sdk1类,包含初始化方法init(Context),实例获取方法getInstance()和对外的服务方法printApplicationName()。为了使用App Startup,需要提供一个初始化器如下:

public class Sdk1Initializer implements Initializer<Sdk1> {
    @NonNull
    @Override
    public Sdk1 create(@NonNull Context context) {
        Sdk1.init(context);
        return Sdk1.getInstance();
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        return Collections.emptyList();
    }
}

泛型T为待初始化的Sdk对外提供的对象类型;create(Context)方法是该Sdk初始化
逻辑写入的地方,其参数context为Application Context,同时需要返回一个Sdk对外提供的对象实例。

dependencies()方法则需要返回一个列表,这个列表需要给出一个该Sdk依赖的其它
Sdk的初始化器,也就是这个列表决定了哪些sdk会在这个sdk之前初始化,如果这个sdk是独立的没有依赖与其它的sdk,可以将该方法返回一个空列表(如Sdk1Initializer的实现)。

但是如果这个sdk依赖于其它的sdk,必须在其它sdk初始化之后才能初始化,则需要在dependencies()方法中指明。例如现在有一个sdk2也需要初始化,且它必须在sdk1初始化之后才能初始化,那么sdk2的初始化器的实现如下:

public class Sdk2Initializer implements Initializer<Sdk2> {
    @NonNull
    @Override
    public Sdk2 create(@NonNull Context context) {
        Sdk2.init(context);
        return Sdk2.getInstance();
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        List<Class<? extends Initializer<?>>> dependencies = new ArrayList<>();
        dependencies.add(Sdk1Initializer.class);
        return dependencies;
    }
}

在dependencies()方法中指明了Sdk2的依赖项,因此App Startup会在初始化sdk2之前先初始化sdk1。

注册Provider和Initializer<?>

我们需要告诉App Startup我们实现了哪些Sdk初始化器(Sdk1Initializer、Sdk2Initializer)。同时App Startup并没有提供AndroidManifest.xml文件,因此App Startup用到的provider同样需要注册。在app的AndroidManifest.xml文件添加如下代码:

<provider
            android:authorities="${applicationId}.androidx-startup"
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="cn.zhengzhengxiaogege.appstartupstudy.Sdk1Initializer"
                android:value="@string/androidx_startup"/>
            <meta-data
                android:name="cn.zhengzhengxiaogege.appstartupstudy.Sdk2Initializer"
                android:value="@string/androidx_startup"/>
</provider>

通常每一个初始化器对应一个<meta-data>标签,但是如果有些初始化器已经被一个已经注册的初始化器依赖(比如Sdk1Initializer已经被Sdk2Initializer依赖),那么
可以不用在AndroidManifest.xml文件中显式地指明,因为App Startup已经通过
注册的Sdk2Initializer找到它了。

这里的<meta-data>标签的value属性必须指定为字符串androidx_startup的值,
也就是("androidx.startup"),否则将不生效。

如果有一个sdk3内部通过App Startup帮助使用者处理了初始化,那么sdk3的AndroidManifest.xml文件中已经存在了InitializationProvider的provider标签,此时会与app模块中的冲突,因此在app模块的provider标签中指明tools:node="merge",通过AndroidManifest.xml文件的合并机制。

/ App Startup实现懒加载 /

为了减少app启动时间,对于一些非必须的初始化应该在app启动后、sdk使用前完成初始化,使用在provider中注册的方法不能达到这个目的。App StartUp提供了AppInitializer来解决这个问题。如下,在需要初始化的位置使用AppInitializer:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, "MainActivity Created");

        AppInitializer.getInstance(getApplicationContext())
                .initializeComponent(Sdk2Initializer.class);

        Sdk1.getInstance().printApplicationName();
    }
}

同时需要修改AndroidManifest.xml文件中的对应初始化器的<meta-data>,如下:

<provider
            android:authorities="${applicationId}.androidx-startup"
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="cn.zhengzhengxiaogege.appstartupstudy.Sdk2Initializer"
                android:value="@string/androidx_startup"
                tools:node="remove"/>
</provider>

通过tools:node="remove"来标记该初始化器。这样会在AndroidManifest.xml文件合并时将这个<meta-data>移除掉,否则该初始化器仍会在Application中被初始化并标记为已经初始化,后面的懒加载将不执行任何初始化操作,相当于使懒加载失效了。

/ 剖析App StartUp /

App StartUp的设计思路比较简单,就是将多个需要初始化的Sdk在一个provider中完成,从而减少多个provider带来的性能问题和繁杂的AndroidManifest.xml文件声明。目前App StartUp为1.0.0-alpha01版本,其代码结构非常简单。



只有五个类文件,除去StartupException和StartupLogger,其核心类只有三个。

Intializer.java的作用很简单,就是为lib的使用者提供了接入的方法,因此不再赘述。

InitializationProvider.java是App StartUp中使用的单一的provider,所有注册的初始化将在这个provider中完成。App StartUp只重写了onCreate()方法:

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

可以看到,在onCreate()方法中执行了扫描和初始化注册的组件。其实现方法在AppInitializer.java中。AppInitializer内部是一个单例实现,它的getInstance(Context)方法传入的是Application级别的Context,并将其传递给注册的各Intializer,首先看discoverAndInitialize()方法:

@SuppressWarnings("unchecked")
void discoverAndInitialize() {
    try {
        Trace.beginSection(SECTION_NAME);

        // 扫描并获取Manifest文件中的 `InitializationProvider`这个组件中注册的<meta-data>
        // 信息
        ComponentName provider = new ComponentName(mContext.getPackageName(),
                InitializationProvider.class.getName());
        ProviderInfo providerInfo = mContext.getPackageManager()
                .getProviderInfo(provider, GET_META_DATA);
        Bundle metadata = providerInfo.metaData;

        // 然后遍历<meta-data>标签,获取到每个标签的 `Initializer`并对其初始化
        String startup = mContext.getString(R.string.androidx_startup);
        if (metadata != null) {
            Set<Class<?>> initializing = new HashSet<>();
            Set<String> keys = metadata.keySet();
            for (String key : keys) {

                // 注意这里会用<meta-data>标签的value属性的值和`@string/androidx_startup`
                // 对比,只有是这个值的<meta-data>标签才会被初始化。
                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));
                        }
                        doInitialize(component, initializing);
                    }
                }
            }
        }
    } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
        throw new StartupException(exception);
    } finally {
        Trace.endSection();
    }
}

discoverAndInitialize()方法首先扫描清单文件获取到需要初始化的初始化器Initializer,然后执行初始化操作,即调用doInitialize(Class<? extends Initializer<?>>, Set<Class<?>>)方法,如下:

@NonNull
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
<T> T doInitialize(
        @NonNull Class<? extends Initializer<?>> component,
        @NonNull Set<Class<?>> initializing) {
    synchronized (sLock) {
        boolean isTracingEnabled = Trace.isEnabled();
        try {
            if (isTracingEnabled) {
                Trace.beginSection(component.getSimpleName());
            }

            // `initializing`存储着正在初始化的初始化器。
            // 这个判断是要解决循环依赖的问题,比如 `Sdk1Initializer`依赖了它本身,或者是
            // `Sdk1Initializer` 依赖了 `Sdk2Initializer`,同时 `Sdk2Initializer` 又
            // 依赖了 `Sdk1Initializer`,这是存在逻辑错误的,因此需要排除。
            if (initializing.contains(component)) {
                String message = String.format(
                        "Cannot initialize %s. Cycle detected.", component.getName()
                );
                throw new IllegalStateException(message);
            }

            // `mInitialized` 是一个 `Map<Class<?>, Object>`,它缓存了已经执行过初始化的
            // `Initializer`的 `Class` 对象和初始化的结果,通过这种方法来避免重复初始化。
            Object result;
            if (!mInitialized.containsKey(component)) {

                // 这是这个初始化器还没被初始化的情况。
                initializing.add(component);
                try {

                    // 首先构造一个该初始化器的实例
                    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) {
                            if (!mInitialized.containsKey(clazz)) {
                                doInitialize(clazz, initializing);
                            }
                        }
                    }

                    if (StartupLogger.DEBUG) {
                        StartupLogger.i(String.format("Initializing %s", component.getName()));
                    }

                    // 调用初始化器的 `create(Context)`方法,执行具体的初始化逻辑。
                    result = initializer.create(mContext);

                    if (StartupLogger.DEBUG) {
                        StartupLogger.i(String.format("Initialized %s", component.getName()));
                    }

                    // 最后把这个初始化器标为已初始化并缓存结果。
                    initializing.remove(component);
                    mInitialized.put(component, result);
                } catch (Throwable throwable) {
                    throw new StartupException(throwable);
                }
            } else {
                // 已经初始化过了,就直接从缓存中取走结果即可。
                result = mInitialized.get(component);
            }
            return (T) result;
        } finally {
            Trace.endSection();
        }
    }
}

doInitialize(Class<? extends Initializer<?>>, Set<Class<?>>)方法首先会实例化一个初始化器,然后通过dependencies()方法找到它依赖的初始化器做递归初始化,这个过程中如果遇到诸如依赖自身、循环依赖等逻辑错误问题将抛出异常。处理完依赖后调用它的create(Context)方法执行具体的初始化逻辑。最后初始化完成,将状态和结果缓存,防止多次初始化。

用来做懒加载的initializeComponent(Class<? extends Initializer<T>>)的方法就比较简单了,它直接调用doInitialize(Class<? extends Initializer<?>>, Set<Class<?>>)方法对指定的初始化器做初始化,如下:

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

/ App Startup利弊 /

优点:

解决了多个sdk初始化导致Application文件和Mainfest文件需要频繁改动的问题,同时也减少了Application文件和Mainfest文件的代码量,更方便维护了
方便了sdk开发者在内部处理sdk的初始化问题,并且可以和调用者共享一个ContentProvider,减少性能损耗。
提供了所有sdk使用同一个ContentProvider做初始化的能力,并精简了sdk的使用流程。
符合面向对象中类的单一职责原则
有效解耦,方便协同开发

缺点:

会通过反射实例化Initializer<>的实现类,在低版本系统中会有一定的性能损耗。
必须给Initializer<>的实现类提供一个无参构造器,当然也不能算是缺点,如果缺少的话新版的android studio会通过lint检查给出提醒。



导致类文件增多,特别是有大量需要初始化的sdk存在时。
版本较低,还没有发行正式版。

以下是我整理出来的一份大厂面试题和程序员进阶资料,有需求的可以点击我的GitHub来获取哦。


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