JetPack之App Startup改造过程
- 组件化过程中,遇到了什么困难?
- 对这困难,你有啥想法?
- App Startup
- App Startup优点缺点
- 改造思路
- 优化体验
- 开源代码
- 参考资料
我们希望将业务划分为一个个的模块(Library),从而更好的组装、拆卸、迭代业务。 通过业务模块在开发时,可能需要接入一些SDK(外部or内部),这些SDK都会要求在Application中初始化。
方案一:指定一个Application类,把这些初始化的代码,写到Application中。
public class GlobalApp extends BaseApplication {
@Override
protected void setup() {
initAllProcessDependencies(this);
if (ProcessUtils.isMainProcess()) {
initMainProcessDependencies();
}
}
public void initAllProcessDependencies(Application app) {
CApp.init(app);
Utils.init(CApp.getApp());
initTinker();
initICBC(app);
}
public void initMainProcessDependencies() {
ClideFactory.init(CApp.getApp(), R.mipmap.image_error);
initReporter();
KV.init(CApp.getApp());
initRouter();
initCrash();
initBugly();
initFPush(UDIDUtils.getUniqueID_NotPermission());
initWOS();
initLocation();
initHeader();
initDeviceId();
initDun();
initFFMpeg();
initStackManage();
}
...
}
方案二:定义一个ContentProvider,将初始化的代码放到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="top.gradle.sdk1">
<application>
<provider
android:authorities="${applicationId}.init-provider"
android:name=".Sdk1InitializeProvider"
android:exported="false"/>
</application>
</manifest>
每一个Library都创建一个自己的ContentProvider,那么…
dependencies {
implementation project(":FirstSdkInitialize")
implementation project(":SecondSdkInitialize")
...
}
最后构建出来的apk就会有多个provider注册,官方称每一个空的provider都会带来2毫秒的启动延时消耗。
项目模块化之后,会存在很多的模块,如果每个模块都创建一个ContentProvider会导致App启动负担过大,而且如果有模块初始化存在依赖关系(比如:B模块初始化,要等A初始化完成后再初始化),这种方案就没法实现了。
方案三:将全类名,写到Application标签的meta-data中,在Application中,去反射这些全类名,从而加载它们。这样还是要将一部分代码放到Application中,仍然很难解决依赖的问题。
这个就不写例子了,Application中先读meta-data的name,通过反射把name中描述的全类名加载出来。
嗯?如果结合方案二和方案三,是不是可以创造一个新东东,既可以不在Application中写代码,又可以不用写很多的ContentProvider…
没错,这就是JetPack家族中App Startup组件的思路,它提供了一个ContentProvider代理类,取代了其它ContentProvider的开销,让项目不会因为ContentProvider越来越多而启动速度越来越慢,然后让其它模块注册meta-data全类名时,通过meger标识放到同一个ContentProvider代理类的上下文中,在ContentProvider代理类中,统一去加载。
App Startup到底能带来多大的提升呢? 这是Google在Pixel2的Android 10上做的测试,每一个ContentProvider的增加,至少会带来2毫秒的消耗,这仅仅是空ContentProvider的附加成本。
那么使用App Startup,在模块越多的项目中,效果越好,它会一定程度的加快App启动速度,而且JetPack家庭中的WorkManager和Lifecycle都是基于它来实现库初始化工作的。
这么优秀,那接下来我们了解一下它怎么用的吧。
- 第一步,让Library依赖app startup库
dependencies {
implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
...
}
public class FirstSdkInitialize {
private Context applicationContext;
private FirstSdkInitialize() {
}
public static FirstSdkInitialize getInstance() {
return FirstSdkInitializeHold.INSTANCE;
}
private static class FirstSdkInitializeHold {
public static final FirstSdkInitialize INSTANCE = new FirstSdkInitialize();
}
public void init(Context applicationContext) {
this.applicationContext = applicationContext.getApplicationContext();
Log.d("cheetah","卢本伟牛逼~");
}
public Context getContext() {
return applicationContext;
}
}
public class FirstInitializeProxy implements Initializer<FirstSdkInitialize> {
@NonNull
@Override
public FirstSdkInitialize create(@NonNull Context context) {
FirstSdkInitialize.getInstance().init(context);
return FirstSdkInitialize.getInstance();
}
@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
return Collections.emptyList();
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wuba.financial.firstsdkinitialize">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.wuba.financial.firstsdkinitialize.FirstInitializeProxy"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
- 上面,第一个Library就写完了,使用同样的方法,写第一个Library,最后让app模块,依赖它俩,就可以运行了。
dependencies {
implementation project(":FirstSdkInitialize")
implementation project(":SecondSdkInitialize")
...
}
我们看到,它是通过一个InitializationProvider类,加载两个模块的meta-data的。
dependencies {
implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
implementation project(":SecondSdkInitialize")
...
}
- 然后在第一个Library的初始化类中,传个依赖关系list给组件。
public class FirstInitializeProxy implements Initializer<FirstSdkInitialize> {
@NonNull
@Override
public FirstSdkInitialize create(@NonNull Context context) {
FirstSdkInitialize.getInstance().init(context);
return FirstSdkInitialize.getInstance();
}
@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
List<Class<? extends Initializer<?>>> dependencies = new ArrayList<>();
dependencies.add(SecondInitializeProxy.class);
return dependencies;
}
}
- 这样顺序初始化就实现了,但…这样Library之间就存在依赖关系啦,咱们组件化的项目,希望组件间尽可能的不产生依赖,咋办…
优点:
- 解决了多个sdk初始化导致Application文件和Mainfest文件需要频繁改动的问题,同时也减少了Application文件和Mainfest文件的代码量,更方便维护了
- 方便了sdk开发者在内部处理sdk的初始化问题,并且可以和调用者共享一个ContentProvider,减少性能损耗。
- 提供了所有sdk使用同一个ContentProvider做初始化的能力,并精简了sdk的使用流程。
- 符合面向对象中类的单一职责原则
- 有效解耦,方便协同开发
缺点:
- 会通过反射实例化Initializer<>的实现类,在低版本系统中会有一定的性能损耗。
- 模块间遇到(B模块初始化,要等A初始化完成后再初始化)这种情况,就会让模块间产生依赖。
- 不支持后台初始化
关于App Startup,请自行官网了解,这里就不再赘述。 下面主要针对缺点,进行一些改造。 它让模块化之后的业务模块之间又产生了依赖关系,这个不能接受。
试着动手改造App Startup
- 先解决(B模块初始化,要等A初始化完成后再初始化)的问题。
- 在初始化类上写个优先级注解
- 在ContentProvider代理类中,拿到Class后,去读优先级,然后排序
- 根据排序好的Class,逐个初始化
@QuickPriority(ModulePriority.LEVEL_2)
public class A extends InitService {
@Override
protected void bindApplication(Context context) {
Log.d("cheetah","看我,我手指放松,我目光如龙,当敌人是空,我战法无穷。"+Thread.currentThread().getId());
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wuba.borrowfinancials.cid">
<application>
<provider
android:name="com.wuba.borrowfinancials.knife.init.ModuleInitProxy"
android:authorities="${applicationId}.module-init"
android:exported="false"
tools:node="merge">
<!-- This entry makes ExampleLoggerInitializer discoverable. -->
<meta-data
android:name="com.wuba.borrowfinancials.cid.CidInitService"
android:value="module.knife" />
</provider>
</application>
</manifest>
public class ModuleInitProxy extends ContentProvider {
@Override
public boolean onCreate() {
try {
ComponentName provider = new ComponentName(mContext.getPackageName(),
ModuleInitProxy.class.getName());
ProviderInfo providerInfo = mContext.getPackageManager()
.getProviderInfo(provider, GET_META_DATA);
Bundle metadata = providerInfo.metaData;
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
List<ModuleInitOrderBean> orderBeans = new ArrayList<>();
Set<String> keys = metadata.keySet();
for (String key : keys) {
String value = metadata.getString(key, null);
if (ModuleInitUtils.MODULE_KNIFE.equals(value)) {
Class<?> clazz = Class.forName(key);
if (InitService.class.isAssignableFrom(clazz)) {
Class<? extends IModuleInitializer> component =
(Class<? extends IModuleInitializer>) clazz;
ModuleInitOrderBean bean = new ModuleInitOrderBean();
bean.setClazz(component);
bean.setPriority(ModuleInitUtils.getPriority(clazz));
orderBeans.add(bean);
}
}
}
Collections.sort(orderBeans);
for (ModuleInitOrderBean bean : orderBeans) {
try {
Object instance = bean.getClazz().getDeclaredConstructor().newInstance();
IModuleInitializer initializer = (IModuleInitializer) instance;
initializer.create(mContext);
initializing.remove(bean.getClazz());
} catch (Throwable throwable) {
throw new ModuleInitException(throwable);
}
}
}
} catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
throw new ModuleInitException(exception);
}
return true;
}
}
- 再提供一种子线程初始化的方法,但通常不建议放子线程中,控制不好就会挂,极限启动优化,可能会用。
- 定义优先级注解为Integer的最大值,则标识需要子线程初始化
- 读取优先级时,将这些Class,单独拿出来,开线程初始化它们。
try {
ComponentName provider = new ComponentName(mContext.getPackageName(),
ModuleInitProxy.class.getName());
ProviderInfo providerInfo = mContext.getPackageManager()
.getProviderInfo(provider, GET_META_DATA);
Bundle metadata = providerInfo.metaData;
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
List<ModuleInitOrderBean> orderBeans = new ArrayList<>();
List<ModuleInitOrderBean> delayBeans = new ArrayList<>();
Set<String> keys = metadata.keySet();
for (String key : keys) {
String value = metadata.getString(key, null);
if (ModuleInitUtils.MODULE_KNIFE.equals(value)) {
Class<?> clazz = Class.forName(key);
if (InitService.class.isAssignableFrom(clazz)) {
Class<? extends IModuleInitializer> component =
(Class<? extends IModuleInitializer>) clazz;
ModuleInitOrderBean bean = new ModuleInitOrderBean();
bean.setClazz(component);
bean.setPriority(ModuleInitUtils.getPriority(clazz));
if (bean.isDelay()) {
delayBeans.add(bean);
} else {
orderBeans.add(bean);
}
}
}
}
Collections.sort(orderBeans);
for (ModuleInitOrderBean bean : orderBeans) {
try {
Object instance = bean.getClazz().getDeclaredConstructor().newInstance();
IModuleInitializer initializer = (IModuleInitializer) instance;
initializer.create(mContext);
initializing.remove(bean.getClazz());
} catch (Throwable throwable) {
throw new ModuleInitException(throwable);
}
}
if (null != delayBeans && !delayBeans.isEmpty()) {
new DelayInitializer().subMit(mContext, delayBeans);
}
}
} catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
throw new ModuleInitException(exception);
}
public class DelayInitializer {
private @NonNull
ExecutorService executorService;
public DelayInitializer() {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("ModuleDelayInitializer", false));
initializing = new HashSet<>();
}
private void addRunnable(Runnable r) {
executorService.submit(r);
}
private @NonNull
Set<Class<?>> initializing;
public void subMit(final Context mContext, final List<ModuleInitOrderBean> list) {
addRunnable(new Runnable() {
@Override
public void run() {
for (final ModuleInitOrderBean bean : list) {
addRunnable(new Runnable() {
@Override
public void run() {
if (initializing.contains(bean.getClazz())) {
String message = String.format(
"Cannot initialize %s. Cycle detected.",
bean.getClazz().getName()
);
throw new IllegalStateException(message);
}
initializing.add(bean.getClazz());
try {
Object instance = bean.getClazz()
.getDeclaredConstructor().newInstance();
IModuleInitializer initializer = (IModuleInitializer) instance;
initializer.create(mContext);
} catch (Throwable throwable) {
throw new ModuleInitException(String.format(
"Cannot initialize %s. not found DeclaredConstructors.",
bean.getClazz().getName()
), throwable);
}
}
});
}
}
});
}
}
清单文件这玩意写的多烦啊,每个模块都得Copy一遍,搞错一个字母都不知道问题出在哪里,除了meta-data的name不一样,其它的都一样,组件啊组件,你能不能帮我搞定这些重复代码块?
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wuba.borrowfinancials.cid">
<application>
<provider
android:name="com.wuba.borrowfinancials.knife.init.ModuleInitProxy"
android:authorities="${applicationId}.module-init"
android:exported="false"
tools:node="merge">
<!-- This entry makes ExampleLoggerInitializer discoverable. -->
<meta-data
android:name="com.wuba.borrowfinancials.cid.CidInitService"
android:value="module.knife" />
</provider>
</application>
</manifest>
好,我开发一个插件,帮你搞定重复的代码。
思路:
- Gradle编译期,拿到Library的清单ProcessManifestTask任务,自动写这玩意。
- meta-data的name变量,通过build.gradle传过来。
最终是这样的:
apply {
from "${rootDir.path}/gradle/output/lib.gradle"
}
ModuleStartup {
initClass = "com.wuba.borrowfinancials.cid.CidInitService"
}
dependencies {
implementation rootProject.ext.dependencies.deviceid
}
Gradle插件编写
- 第一步,拿到Library的清单ProcessManifestTask的Hook点。
class ModuleStartup implements Plugin<Project> {
ModuleStartupExtension moduleStartupExtension
@Override
void apply(Project project) {
if (!project.plugins.hasPlugin('com.android.library')) {
throw new GradleException('ModuleStartup: Android library plugin required')
}
project.extensions.create("ModuleStartup", ModuleStartupExtension)
moduleStartupExtension = project['ModuleStartup']
def android = project.extensions.android
android.libraryVariants.all { variant ->
String variantName = variant.name.capitalize()
getProcessManifestTask(project, variantName).doLast {
println "${AppConstant.TAG} processManifest: ${it.outputs.files} "
it.outputs.files.each { File file ->
}
}
}
}
static Task getProcessManifestTask(Project project, String variantName) {
String mergeManifestTaskName = "process${variantName}Manifest"
return project.tasks.findByName(mergeManifestTaskName)
}
}
def static final START_MANIFEST_TAG = "<manifest"
def static final TOOLS_TAG = "xmlns:tools=\"http://schemas.android.com/tools\""
def static final APPLICATION_TAG = "<application"
def static final MANIFEST_END_TAG = "</manifest>"
def static final APPLICATION_END_TAG = "</application>"
def static final MANIFEST_EMPTY_END = "/>"
def static final provider_name = "android:name"
def static final authorities = "android:authorities"
def static final exported = "android:exported"
def static final value = "android:value"
def static final node = "tools:node"
def static final provider_name_value = "com.wuba.financial.base.module.startup.ModuleInitProxy"
def static final authorities_value = ".module-init"
def static final exported_value = "false"
def static final node_value = "merge"
def static final meta_value = "module.knife"
def static generatorProviderTag(Boolean hasApplication, String applicationID, String initClass) {
def strXml = new StringWriter()
MarkupBuilder mb = new MarkupBuilder(strXml)
if (hasApplication) {
mb.provider(
"${provider_name}": "${provider_name_value}",
"${authorities}": "${applicationID}${authorities_value}",
"${exported}": "${exported_value}",
"${node}": "${node_value}") {
"meta-data"(
"${provider_name}": "${initClass}",
"${value}": "${meta_value}"
)
}
} else {
mb.application {
provider(
"${provider_name}": "${provider_name_value}",
"${authorities}": "${applicationID}${authorities_value}",
"${exported}": "${exported_value}",
"${node}": "${node_value}") {
"meta-data"(
"${provider_name}": "${initClass}",
"${value}": "${meta_value}"
)
}
}
}
strXml
}
def appendManifest(def file) {
if (file == null || !file.exists()) return
println "${AppConstant.TAG} appendManifest: ${file}"
ManifestStatBean bean = Presenter.parseManifestStat(file)
if (!bean.hasStartManifest) {
println "${file} not found Manifest Tag"
return
}
String updatedContent = file.getText("UTF-8")
if (!bean.hasTools) {
updatedContent = Presenter.writeTools(updatedContent)
}
String content = Presenter.generatorProviderTag(
bean.hasApplication
, moduleStartupExtension.applicationId
, moduleStartupExtension.initClass)
if (bean.hasApplication) {
if (bean.hasEndApplication) {
updatedContent = Presenter.replaceEndApplicationTag(updatedContent, content)
} else {
updatedContent = Presenter.replaceApplicationEmptyEndTag(updatedContent, content)
}
} else {
if (bean.hasEndManifest) {
updatedContent = Presenter.replaceEndManifest(updatedContent, content)
} else {
updatedContent = Presenter.replaceManifestEmptyEndTag(updatedContent, content)
}
}
file.write(updatedContent, 'UTF-8')
}
-
ModuleStartup:http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartup -
ModuleStartupPlugin(可选插件):http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartupPlugin -
Demo:http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartupDemo -
注:要再造轮子的各位,请阅读完整源代码,除了核心代码外,还是很多坑在非核心代码中。
- app-startup官方:https://developer.android.com/topic/libraries/app-startup
|