Prefacio
La tecnología de complementos ha estado floreciendo desde 2015, como: el replugin de Qihoo 360, el VirtualAPK de Didi y ahora VirtualApp. La tecnología de complementos se ha sometido a pruebas severas en el mercado y está madurando gradualmente. Hoy, llevaré a todos a implementar un complemento -en. Cambiar el marco de actividades, espero que te ayude ~
Concepto de complemento
El complemento es una tecnología que carga dinámicamente los cuatro componentes principales. Al principio, era para resolver el problema de la limitación 65535, más tarde, Google salió multidex para resolver específicamente el problema del
uso de complementos en el mercado para reducir el tamaño del paquete de instalación hasta cierto punto, realizar la componenteización del proyecto, proyectos separados para facilitar el aislamiento y reducir el acoplamiento de componentes.El problema
, por supuesto, la tecnología de plug-in puede lograr correcciones de errores térmicos, debido a la presencia de la máquina virtual, Java admite la carga dinámica de cualquier clase. Es solo que el sistema Android impone restricciones a los cuatro componentes principales, cuando intentas abrir un componente que no está en la lista, te fallará.
El llamado complemento es esencialmente para evitar esta restricción, de modo que la aplicación pueda abrir y usar libremente los cuatro componentes principales.
Valor comercial de complemento
El complemento no es más que resolver el problema de la carga de clases y la carga de recursos. La carga de recursos generalmente se realiza mediante la reflexión AssertManager. De acuerdo con la carga de clases, el complemento generalmente se divide en métodos de enlace y proxy estático. Generalmente se usa el complemento para solucionar la lentitud de la cobertura de nuevas versiones de aplicaciones El problema.
Los cuatro componentes principales se pueden cargar dinámicamente, lo que significa que los usuarios no necesitan instalar manualmente una nueva versión de la aplicación. También podemos proporcionar a los usuarios nuevas funciones y páginas, o corregir errores sin que los usuarios se sientan.
Estructura del proyecto de complemento
Proceso de desarrollo de complementos
Paso 1: cree el proyecto de la aplicación principal como proyecto anfitrión
Paso 2: Cree plugin_package como un proyecto de complemento, responsable de abrir el paquete de complemento
Paso 3: Cree el proyecto de interfaz lifecycle_manager, responsable de administrar el ciclo de vida de los cuatro componentes principales
Paso 4: instale el complemento
4.1 Copie los archivos en Activos al directorio / data / data / files
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
4.2 Construir DexClassLoader a través de proxy estático
Debido a que no hay un entorno de contexto, el host debe proporcionar el entorno de contexto, un DexClassLoader contiene un complemento,
// 获取插件目录下的文件
File extractFile = mContext.getFileStreamPath(mApkName);
// 获取插件包路径
String dexPath = extractFile.getPath();
// 创建Dex输出路径
File fileRelease = mContext.getDir("dex", Context.MODE_PRIVATE);
// 构建 DexClassLoader 生成目录
mPluginClassLoader = new DexClassLoader(dexPath,
fileRelease.getAbsolutePath(), null, mContext.getClassLoader());
El método Hook es fusionar el archivo dex en el DexClassLoader del host, pero la actividad que omite el registro del archivo de manifiesto AMS arrojará ClassNotFuoundException, por lo que se requieren Hook startActivity y handleResumeActivity. El primero es simple de implementar y tiene buena compatibilidad, y el complemento -in está separado, este último Mala compatibilidad y fácil desarrollo, pero si varios complementos tienen la misma clase, se producirán problemas. Aquí se utiliza un proxy estático.
4.3 Realice la carga de recursos a través de la reflexión AssertManager
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method method = AssetManager.class.getMethod("addAssetPath", String.class);
method.invoke(assetManager, dexPath);
mPluginResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
} catch (Exception e) {
Toast.makeText(mContext, "加载 Plugin 失败", Toast.LENGTH_SHORT).show();
}
Paso 5: analizar el complemento
La implementación del proxy estático es muy simple. No es necesario que esté familiarizado con el proceso de inicio de la actividad. Está directamente orientado a la programación de la interfaz. Primero, debe cargar el complemento en la aplicación host para construir DexClassCloder y Resource objetos. Con DexClassLoader, puede cargar la clase Resource en el complemento reflejando AssertManager. El addAssertPath crea un AssertManager y luego construye un objeto Resource. Por supuesto, iniciar el Servicio y registrar la transmisión dinámica son en realidad lo mismo que iniciar la Actividad. todo comenzó a través del contexto del host, pero el marco de DL no admite la transmisión estática. La transmisión estática se analiza y registra cuando se instala la aplicación, y el manifiesto de nuestro complemento no se puede registrar, por lo que la transmisión estática en el interior solo se puede analizar y registrar manualmente, utilizando la reflexión para llamar al método parsePackage de PackageParser. Convertir static transmisiones a transmisiones dinámicas. La implementación específica está en la implementación del método PluginManager # parserApkAction
public void parserApkAction() {
try {
Class packageParserClass = Class.forName("android.content.pm.PackageParser");
Object packageParser = packageParserClass.newInstance();
Method method = packageParserClass.getMethod("parsePackage", File.class, int.class);
File extractFile = mContext.getFileStreamPath(mApkName);
Object packageObject = method.invoke(packageParser, extractFile, PackageManager.GET_RECEIVERS);
Field receiversFields = packageObject.getClass().getDeclaredField("receivers");
ArrayList arrayList = (ArrayList) receiversFields.get(packageObject);
Class packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Class userHandleClass = Class.forName("android.os.UserHandle");
int userId = (int) userHandleClass.getMethod("getCallingUserId").invoke(null);
for (Object activity : arrayList) {
Class component = Class.forName("android.content.pm.PackageParser$Component");
Field intents = component.getDeclaredField("intents");
// 1.获取 Intent-Filter
ArrayList<IntentFilter> intentFilterList = (ArrayList<IntentFilter>) intents.get(activity);
// 2.需要获取到广播的全类名,通过 ActivityInfo 获取
// ActivityInfo generateActivityInfo(Activity a, int flags, PackageUserState state, int userId)
Method generateActivityInfoMethod = packageParserClass
.getMethod("generateActivityInfo", activity.getClass(), int.class,
packageUserStateClass, int.class);
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, activity, 0,
packageUserStateClass.newInstance(), userId);
Class broadcastReceiverClass = getClassLoader().loadClass(activityInfo.name);
BroadcastReceiver broadcastReceiver = (BroadcastReceiver) broadcastReceiverClass.newInstance();
for (IntentFilter intentFilter : intentFilterList) {
mContext.registerReceiver(broadcastReceiver, intentFilter);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
Con el objeto AssertManager, puede acceder al archivo de recursos, pero el complemento no tiene un entorno de contexto de contexto. El host debe proporcionarle este entorno de contexto. El método específico es obtener la anotación de actividad del complemento. -en la entrada a través de PackManager e inyecte el contexto de host, que completa los Pasos de la aplicación de host para saltar a la aplicación de complemento. Pero la aplicación de complemento no tiene un entorno de contexto, por lo que la aplicación de complemento no puede iniciar directamente la actividad, es necesario que el contexto del host inicie la actividad.
El sexto paso, actividad de proxy: construir ActivityInterface en lifecycle_mananager para administrar el ciclo de vida de la actividad del complemento
public interface ActivityInterface {
// 插入Activity上下文
void insertAppContext(Activity hostActivity);
// Activity各个生命周期方法
void onCreate(Bundle savedInstanceState);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
}
El séptimo paso, actividad de proxy: Construya BaseActivity en plugin_package para implementar ActivityInterface
Proporcione startActivity en BaseActivity y tírelo a la actividad del host para comenzar
public void startActivity(Intent intent) {
Intent newIntent = new Intent();
newIntent.putExtra("ext_class_name", intent.getComponent().getClassName());
mHostActivity.startActivity(newIntent);
}
El octavo paso, actividad de proxy: cree el complemento de actividad en plugin_package
public class PluginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start).setOnClickListener(
v -> startActivity(new Intent(mHostActivity, TestActivity.class))
);
}
}
// 测试插件Activity
public class TestActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
}
Paso 9: Inicie la actividad de entrada del complemento
Lo principal en este paso es registrar un contexto de host para el complemento
// PorxyActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取到真正要启动的插件 Activity,然后执行 onCreate 方法
String className = getIntent().getStringExtra(EXT_CLASS_NAME);
try {
Class clazz = getClassLoader().loadClass(className);
ActivityInterface activityInterface = (ActivityInterface) clazz.newInstance();
// 注册宿主的 Context
activityInterface.insertAppContext(this);
activityInterface.onCreate(savedInstanceState);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra(EXT_CLASS_NAME);
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra(EXT_CLASS_NAME, className);
super.startActivity(proxyIntent);
}
De esta forma se ha completado el inicio de PluginActivity, pero cabe destacar que en la Actividad del plug-in ya no podemos usar este, porque el plug-in no tiene un entorno de contexto, por lo que algunos métodos de El contexto de llamada necesita usar el contexto del host. Ejecución, como:
Proporcione findViewById en BaseActivity, puede encontrar el archivo de identificación de diseño
public View findViewById(int layoutId) {
return mHostActivity.findViewById(layoutId);
}
Proporcione setContentView en BaseActivity para facilitar la representación del diseño de la interfaz de usuario
public void setContentView(int resId) {
mHostActivity.setContentView(resId);
}
Introducción al principio de plug-in
-
Use DexClassLoader para cargar la Apk del complemento
-
Ejecute la actividad en el complemento a través de la actividad del agente y cargue el ciclo de vida correspondiente.
-
Llame a addAssetPath de AssetManager a través de la reflexión para cargar los recursos en el complemento
Problemas encontrados en la plug-inización
La actividad encontrada no está en el paquete de complementos
Lo que realmente abrimos es una Actividad definida en el paquete de complementos La información que esta Actividad necesita está en el paquete de complementos, no en el host.
solución
La actividad del complemento también reescribe el método attachBaseContext. En este paso, cree un contexto propio con el cargador de clases del complemento y la instancia de Resources, reemplace el contexto base con él y páselo a la clase principal para su almacenamiento. De esta forma, cuando la empresa llama a getClassLoader () o getResources (), se obtiene toda la información del complemento.
No se encontró la falta de coincidencia del tipo de ID de recurso
Cuando necesita obtener un dibujable por un ID de recurso, lo que obtiene es color u otros recursos
solución
Ocurre principalmente en versiones inferiores a 8.0. Después de la investigación, se encuentra que en los paquetes de complementos por debajo de 8.0, ContextThemeWrapper.mResources es el recurso del host, no el recurso del complemento. Como resultado, los recursos encontrados con el mismo ID no se corresponden.
Bloqueo causado por una fuga del paquete del complementocanary
El leakingcanary usará el recurso de actividad en la parte superior de la pila para cargar una imagen que desea mostrar, pero es posible que este recurso no esté en el complemento actual.
solución
El host y todos los complementos dependen de leakingcanary.
para resumir
Este artículo se basa principalmente en mi propia práctica de componenteización de complementos que realmente puse en producción, y comparto algunos problemas que deben tenerse en cuenta al cargar dinámicamente complementos SDK. El contenido incluye principalmente los problemas comunes de la solución del complemento, el bloqueo causado por el filtro de fugas del paquete del complemento, el tipo de ID del recurso no coincide y la actividad del host no puede encontrar el problema. Miles de palabras entran en una frase:
Los complementos son riesgosos, ¡la inversión debe ser cautelosa!
Autor: pequeña caja de madera
enlace: https: //juejin.cn/post/6897476017527783437
Fuente: Nuggets
copyright reservado por los autores. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización. Para reimpresiones no comerciales, indique la fuente.