Dibujar a mano un marco de actividad de complemento

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

  1. Use DexClassLoader para cargar la Apk del complemento

  2. Ejecute la actividad en el complemento a través de la actividad del agente y cargue el ciclo de vida correspondiente.

  3. 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.

Supongo que te gusta

Origin blog.csdn.net/qq_39477770/article/details/110073904
Recomendado
Clasificación