Guía de acceso al complemento Android Tencent Shadow

Guía de acceso al complemento Android Tencent Shadow

Breve introducción del complemento

No mencionaré el concepto de complemento, ¡puede Baidu usted mismo!
El punto de dolor del complemento es el archivo de manifiesto AndroidManifest.xml. Si no está registrado en el archivo de manifiesto, no se requiere tecnología avanzada y se puede realizar a través de la reflexión y otros métodos, lo cual es muy simple. Por lo tanto, muchos marcos de complementos en el mercado se implementan ocupando espacio, sin embargo, debido al problema de los permisos de API privados después de Android 9.0, los marcos en el mercado básicamente han dejado de mantenerlos. Soporte hasta 9.0. Entonces, la pregunta es, ¿cómo es suficiente admitir solo 9.0? ¡Android ya tiene 13, y pronto cumplirá 14! Así que hoy les presentaré el acceso de la sombra. No repetiré su introducción y breve descripción, pero le daré algunas URL:

Análisis del autor en la sombra: https://juejin.cn/user/536217405890903/posts
Video de la estación B grabado por otros: https://www.bilibili.com/video/BV1u14y1f7v8/?spm_id_from=333.337.search-card.all.click&vd_source =0f8f0025ace2f1265a86bba19aa4778d
Blog escrito por otros:
https://www.jianshu.com/p/f00dc837227f
Esta demostración: https://github.com/fzkf9225/shadow-plugin-master

1. almacén de clones

Primero, clonamos el código oficial del almacén:
https://github.com/Tencent/Shadow

2. Compile y ejecute la demostración oficial

Use el comando en el directorio Shadow para asegurarse de que esté en el entorno Java8:

./gradlew build

O haga clic directamente en el archivo de secuencia de comandos en su directorio raíz del proyecto

gradlew.bat

3. Publique Shadow en nuestro almacén local

3.1 Instalar la versión Nexus 3.x

Puede descargar un nexus, la operación específica puede realizarla Baidu, 3.x Nexus es muy tonto, recuerde configurar el certificado https y el puerto: https://help.sonatype.com/repomanager3/product-information/download

3.2 Modificar la configuración de lanzamiento

Modifique las configuraciones maven.gradle y common.gradle en el marco Shadow, en buildScript en el directorio raíz. Hay configuraciones relacionadas con github, que se pueden cambiar o no. No es importante. Lo principal es modificar las siguientes partes porque el código fuente está disponible, así que tomé una captura de pantalla directamente: primero puede modificar la versión de
inserte la descripción de la imagen aquí
lanzamiento o otra información en la ruta buildScripts/gradle/common.gradle :

ext.ARTIFACT_VERSION = ext.VERSION_NAME + ext.VERSION_SUFFIX

Modifique directamente al número de versión que desee:

ext.ARTIFACT_VERSION = 1.0

3.3 Almacén de liberación

Cómo publicar en el almacén maven de nexus usando comandos o por ti mismo

./gradlew publish

3.4 Pack almacén de referencia

Después de que el lanzamiento sea exitoso, puede encontrar el archivo pom en el directorio de Shadow framework como se muestra en la figura. Me
inserte la descripción de la imagen aquí
temo que algunas personas no saben cómo usarlo. Permítanme decir una cosa más:
agregar a la compilación .gradle que necesita ser referenciado:

##对应上图
implementation "groupId:artifactId:version"

Ejemplo:

implementation "com.tencent.shadow.core:common:1.0"

Para evitar una gran cantidad de números de versión modificados causados ​​por actualizaciones retroactivas, podemos agregar una configuración global shadow_version.Tenga en cuenta que cuando el sistema importa paquetes, hay comillas simples y debemos cambiar los parámetros de referencia a comillas dobles.

#错误
implementation 'com.tencent.shadow.core:common:$shadow_version'
#正确
implementation "com.tencent.shadow.core:common:$shadow_version"

4. Escribir nuestro propio código

4.1 Crear un nuevo proyecto e importar configuraciones comunes como maven

La estructura del proyecto se muestra en la figura (solo como referencia):
inserte la descripción de la imagen aquí

  • nombre del paquete de la aplicación: Recomendado: com.nombre de dominio.nombre del proyecto, ejemplo: com.casic.titan.shadow
  • común: nada que explicar, solo el módulo del paquete básico
  • constante: módulo de parámetros estáticos, de hecho, no puede usarlo, pero varios módulos deben escribir sus propios valores mágicos, lo cual es fácil de cometer errores, por lo que es mejor agregar un módulo
  • nombre del paquete de la aplicación de complemento: recomendación: com.nombre de dominio.nombre del proyecto, ejemplo: com.casic.titan.shadow, para demostrar la recomendación oficial y el nombre del paquete de calidad consistente
  • pliugin-loader: Recomendado: com.domain name.loader, ejemplo: com.casic.titan.loader
  • plugin-manager: Recomendado: com.domain name.manager, ejemplo: com.casic.titan.manager
  • plugin-runtime: Recomendado: com.domain name.runtime, ejemplo: com.casic.titan.runtime
  • plugin-user: Recomendado: nombre de dominio com. Siéntase libre, ejemplo: com.casic.titan.user, recuerde agregar la configuración del script en gradle (se mencionará más adelante)

4.1.1 Importar script de compilación

Copie los archivos common.gradle y version.gradle en el directorio buildScript bajo el proyecto shadow. De hecho, no necesita copiarlos, pero agregar módulos públicos ahorrará muchas cosas. Después de copiar, modificamos levemente common. grada de la siguiente manera
:

def gitShortRev() {
    
    
    def gitCommit = ""
    def proc = "git rev-parse --short HEAD".execute()
    proc.in.eachLine {
    
     line -> gitCommit = line }
    proc.err.eachLine {
    
     line -> println line }
    proc.waitFor()
    return gitCommit
}

allprojects {
    
    
    ext.COMPILE_SDK_VERSION = 31
    ext.MIN_SDK_VERSION = 24
    ext.TARGET_SDK_VERSION = 31
    ext.VERSION_CODE = 1

    if ("${System.env.CI}".equalsIgnoreCase("true")) {
    
    
        ext.VERSION_NAME = System.getenv("GITHUB_REF_SLUG")
    } else {
    
    
        ext.VERSION_NAME = "local"
    }
	//System.env是获取window环境变量,不用管他
    if ("${System.env.PUBLISH_RELEASE}".equalsIgnoreCase("true")) {
    
    
        ext.VERSION_SUFFIX = ""
    } else if ("${System.env.CI}".equalsIgnoreCase("true")) {
    
    
        ext.VERSION_SUFFIX = "-${System.env.GITHUB_SHA_SHORT}-SNAPSHOT"
    } else {
    
    
        ext.VERSION_SUFFIX = "-${gitShortRev()}-SNAPSHOT"
    }
    ext.ARTIFACT_VERSION = ext.VERSION_NAME + ext.VERSION_SUFFIX
    ext.TEST_HOST_APP_APPLICATION_ID = 'com.casic.titan.test.shadow'//你测试的applicationId
    ext.SAMPLE_HOST_APP_APPLICATION_ID = 'com.casic.titan.shadow'//你项目的applicationId
}

version.gradle es el siguiente

constraintlayoutVersion=2.1.3
materialVersion=1.5.0
appcompatVersion=1.4.1
espressoCoreVersion=3.4.0
shadow_version=1.0
compileSdk=31
minSdk=24
targetSdk=31
javassist_version=3.28.0-GA

4.1.2 Modificar la versión de gradle y la versión del paquete del complemento

Ahora, la nueva versión de los proyectos recién creados de Android Studio tiene por defecto la versión 8.0, pero Shadow no la admite. Tenemos que modificarla.

a) Modifique builde.gradle en el directorio raíz del proyecto

plugins {
    
    
    id 'com.android.application' version '7.0.3' apply false
}

b) archivo gradle-wrapper.properties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

4.1.3 Agregar dependencia maven

Abra el archivo setting.gradle

pluginManagement {
    
    
    repositories {
    
    
        maven {
    
    setUrl("https://localhost:9224/repository/casic_group/")}
        maven {
    
     setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
        google()
        mavenLocal()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    
    
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
    
    
        maven {
    
    setUrl("https://localhost:9224/repository/casic_group/")}
        maven {
    
     setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
        google()
        mavenLocal()
        mavenCentral()
    }
}

Preste especial atención a lo anterior: Asegúrese de agregar

mavenLocal()

Entre ellos, https://localhost:9224/repository/casic_group/ es la dirección de mi máquina local, no puedes usarla, cámbiala por la tuya, debe ser puerto https

4.2 Realizar el módulo host

4.2.1 Agregar dependencias

Agregue en el nodo del directorio de Android en build.gradle

    sourceSets {
    
    
        debug {
    
    
            assets.srcDir('build/generated/assets/plugin-manager/debug/')
            assets.srcDir('build/generated/assets/plugin-zip/debug/')
        }
        release {
    
    
            assets.srcDir('build/generated/assets/plugin-manager/release/')
            assets.srcDir('build/generated/assets/plugin-zip/release/')
        }
    }
    dataBinding{
    
    
        enabled = true
    }
    lintOptions {
    
    
        checkReleaseBuilds false
        abortOnError false
    }

Agregar dependencias

 implementation project(":constant")
    implementation "com.tencent.shadow.core:common:$shadow_version"//AndroidLogLoggerFactory
    implementation 'commons-io:commons-io:2.9.0'//sample-host从assets中复制插件用的
    implementation "com.tencent.shadow.dynamic:host:$shadow_version"//腾讯插件框架shadow
    implementation "com.tencent.shadow.dynamic:host-multi-loader-ext:$shadow_version"//腾讯插件框架shadow

Recuerde modificar algunos números de versión unificados de compileSdk, minSdk, etc., que pueden ignorarse aquí

    compileSdk project.COMPILE_SDK_VERSION

    defaultConfig {
    
    
        applicationId "你的applicationId"
        minSdk project.MIN_SDK_VERSION
        targetSdk project.TARGET_SDK_VERSION
        versionCode 1
        versionName "1.0"
        multiDexEnabled = true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

Recuerde modificar la versión de la biblioteca dependiente, preste atención al problema de las comillas simples y dobles, que también se pueden ignorar aquí según los requisitos.

    implementation "androidx.appcompat:appcompat:$appcompatVersion"
    implementation "com.google.android.material:material:$materialVersion"
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion"

4.2.2 Código de rollo

Cree una nueva aplicación, recuerde configurarla en el archivo de manifiesto

public class MyApplication extends Application {
    
    
    private static MyApplication sApp;

    private PluginManager mPluginManager;

    @Override
    public void onCreate() {
    
    
        super.onCreate();
        sApp = this;

        detectNonSdkApiUsageOnAndroidP();
        setWebViewDataDirectorySuffix();
        LoggerFactory.setILoggerFactory(new AndroidLoggerFactory());

        if (isProcess(this, ":plugin")) {
    
    
            //在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
            //为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
            //因此这里恢复加载上一次的runtime
            DynamicRuntime.recoveryRuntime(this);
        }

        if (isProcess(this, getPackageName())) {
    
    
            PluginHelper.getInstance().init(this);
        }

//        HostUiLayerProvider.init(this);
    }

    private static void setWebViewDataDirectorySuffix() {
    
    
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
    
    
            return;
        }
        WebView.setDataDirectorySuffix(Application.getProcessName());
    }

    private static void detectNonSdkApiUsageOnAndroidP() {
    
    
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
    
    
            return;
        }
        StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
        builder.detectNonSdkApiUsage();
        StrictMode.setVmPolicy(builder.build());
    }

    public static MyApplication getApp() {
    
    
        return sApp;
    }

    public void loadPluginManager(File apk) {
    
    
        if (mPluginManager == null) {
    
    
            mPluginManager = Shadow.getPluginManager(apk);
        }
    }

    public PluginManager getPluginManager() {
    
    
        return mPluginManager;
    }

    private static boolean isProcess(Context context, String processName) {
    
    
        String currentProcName = "";
        ActivityManager manager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
    
    
            if (processInfo.pid == myPid()) {
    
    
                currentProcName = processInfo.processName;
                break;
            }
        }

        return currentProcName.endsWith(processName);
    }
}

Nueva clase auxiliar de complemento PluginHelper

public class PluginHelper {
    
    
    private static final Logger mLogger = LoggerFactory.getLogger(PluginHelper.class);

    public final static String sPluginManagerName = "plugin-manager.apk";//动态加载的插件管理apk

    /**
     * 动态加载的插件包,里面包含以下几个部分,插件apk,插件框架apk(loader apk和runtime apk), apk信息配置关系json文件
     */
    public final static String sPluginZip = DEBUG ? "plugin-debug.zip" : "plugin-release.zip";
    public File pluginManagerFile;
    public File pluginZipFile;
    public ExecutorService singlePool = Executors.newSingleThreadExecutor();
    private Context mContext;

    private static PluginHelper sInstance = new PluginHelper();
    public static PluginHelper getInstance() {
    
    
        return sInstance;
    }

    private PluginHelper() {
    
    
    }

    public void init(Context context) {
    
    
        pluginManagerFile = new File(context.getFilesDir(), sPluginManagerName);
        pluginZipFile = new File(context.getFilesDir(), sPluginZip);
        mLogger.debug("pluginManagerFile:"+pluginManagerFile);
        mLogger.debug("pluginZipFile:"+pluginZipFile);
        mContext = context.getApplicationContext();
        singlePool.execute(() -> preparePlugin());
    }

    private void preparePlugin() {
    
    
        try {
    
    
            InputStream is = mContext.getAssets().open(sPluginManagerName);
            FileUtils.copyInputStreamToFile(is, pluginManagerFile);
            InputStream zip = mContext.getAssets().open(sPluginZip);
            FileUtils.copyInputStreamToFile(zip, pluginZipFile);
        } catch (IOException e) {
    
    
            throw new RuntimeException("从assets中复制apk出错", e);
        }
    }
}

Cree una nueva clase de registro AndroidLoggerFactory, no hay nada que modificar, solo copie el código fuente, es demasiado largo.
Cree una nueva clase de sombra

public class Shadow {
    
    
    public static PluginManager getPluginManager(File apk){
    
    
        final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
        File tempPm = fixedPathPmUpdater.getLatest();
        if (tempPm != null) {
    
    
            return new DynamicPluginManager(fixedPathPmUpdater);
        }
        return null;
    }
}

Nuevo FixedPathPmUpdater

import com.tencent.shadow.dynamic.host.PluginManagerUpdater;

import java.io.File;
import java.util.concurrent.Future;

public class FixedPathPmUpdater implements PluginManagerUpdater {
    
    

    final private File apk;

    public FixedPathPmUpdater(File apk) {
    
    
        this.apk = apk;
    }

    /**
     * @return <code>true</code>表示之前更新过程中意外中断了
     */
    @Override
    public boolean wasUpdating() {
    
    
        return false;
    }
    /**
     * 更新
     *
     * @return 当前最新的PluginManager,可能是之前已经返回过的文件,但它是最新的了。
     */
    @Override
    public Future<File> update() {
    
    
        return null;
    }
    /**
     * 获取本地最新可用的
     *
     * @return <code>null</code>表示本地没有可用的
     */
    @Override
    public File getLatest() {
    
    
        return apk;
    }
    /**
     * 查询是否可用
     *
     * @param file PluginManagerUpdater返回的file
     * @return <code>true</code>表示可用,<code>false</code>表示不可用
     */
    @Override
    public Future<Boolean> isAvailable(final File file) {
    
    
        return null;
    }
}

Cree un nuevo servicio servicio, un servicio representa un complemento, puede crear algunos complementos, porque escribí dos complementos, así que escribí dos servicios

import com.tencent.shadow.dynamic.host.PluginProcessService;

/**
 * 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
 * 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
 */
public class MainPluginProcessService extends PluginProcessService {
    
    
}

import com.tencent.shadow.dynamic.host.PluginProcessService;

/**
 * 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
 * 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
 */
public class UserPluginProcessService extends PluginProcessService {
    
    
}

El siguiente paso es configurar el archivo de manifiesto.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:name=".base.MyApplication"
        android:theme="@style/Theme.Shadowpluginmaster">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!--container 注册
        注意configChanges需要全注册
        theme需要注册成透明

        这些类不打包在host中,打包在runtime中,以便减少宿主方法数增量
        Activity 路径需要和插件中的匹配,后面会说到
        -->
        <!--这三个Activity的报名不是当前的报名而是module runtime下的,这个后面会说到,这样写就完事了-->
        <activity
            android:name="com.casic.titan.plugin_runtime.PluginDefaultProxyActivity"
            android:launchMode="standard"
            android:screenOrientation="portrait"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
            android:hardwareAccelerated="true"
            android:theme="@style/PluginContainerActivity"
            android:exported="true"
            android:multiprocess="true" />

        <activity
            android:name="com.casic.titan.plugin_runtime.PluginSingleInstance1ProxyActivity"
            android:launchMode="singleInstance"
            android:screenOrientation="portrait"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
            android:hardwareAccelerated="true"
            android:exported="true"
            android:theme="@style/PluginContainerActivity"
            android:multiprocess="true" />

        <activity
            android:name="com.casic.titan.plugin_runtime.PluginSingleTask1ProxyActivity"
            android:launchMode="singleTask"
            android:screenOrientation="portrait"
            android:exported="true"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
            android:hardwareAccelerated="true"
            android:theme="@style/PluginContainerActivity"
            android:multiprocess="true"  />

  

        <provider
            android:authorities="${applicationId}.contentprovider.authority.dynamic"
            android:exported="true"
            android:name="com.tencent.shadow.core.runtime.container.PluginContainerContentProvider"
            android:grantUriPermissions="true"
            android:process=":plugin" />

	<!--记得修改进程,不同插件不能运行在同一进程,不然数据肯定泄露了-->
        <service
            android:name=".plugin_manager.MainPluginProcessService"
            android:exported="true"
            android:process=":plugin" />
            <!--记得修改进程,不同插件不能运行在同一进程,不然数据肯定泄露了-->
        <service
            android:name=".plugin_manager.UserPluginProcessService"
            android:exported="true"
            android:process=":plugin_user" />
    </application>

</manifest>

Complemento de temaContainerActivity

    <style name="PluginContainerActivity" parent="@android:style/Theme.NoTitleBar.Fullscreen">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>

4.3 Módulo de escritura de constante de parámetro estático

Crear una nueva clase directamente

final public class Constant {
    
    
    public static final String KEY_PLUGIN_ZIP_PATH = "pluginZipPath";
    public static final String KEY_ACTIVITY_CLASSNAME = "KEY_ACTIVITY_CLASSNAME";
    public static final String KEY_EXTRAS = "KEY_EXTRAS";
    public static final String KEY_PLUGIN_NAME = "key_plugin_name";
    public static final String KEY_PLUGIN_PART_KEY = "KEY_PLUGIN_PART_KEY";
    public static final String PLUGIN_APP_NAME = "plugin-app";//建议者两个参数一致也和module名保持一致,建议但非必须
    public static final String PART_KEY_PLUGIN_BASE = "plugin-app";  //part-key  和 plugin-app  build.gradle中一致
    public static final String PLUGIN_USER_NAME = "plugin-user";//建议者两个参数一致也和module名保持一致,建议但非必须
    public static final String PART_KEY_PLUGIN_USER = "plugin-user";  //part-key  和 plugin-app  build.gradle中一致
    public static final int FROM_ID_NOOP = 1000;
    public static final long FROM_ID_START_ACTIVITY = 1002;//标识启动的是Activity
    public static final int FROM_ID_CALL_SERVICE = 1001;//标识启动的是Service
    public static final int FROM_ID_CLOSE = 1003;
    public static final int FROM_ID_LOAD_VIEW_TO_HOST = 1004;

}

build.grale-configuration

plugins {
    
    
//修改为library
    id 'com.android.library'
}

4.4, implementación del módulo cargador de complementos

Importar bibliotecas dependientes en build.gradle

	//自带的可以删除只保留这几个,不是一定要删,随你,但是你删除库会导致资源文件引用不到res下也要删,还有清单文件
    implementation "com.tencent.shadow.dynamic:loader-impl:$shadow_version"
    compileOnly "com.tencent.shadow.core:activity-container:$shadow_version"
    compileOnly "com.tencent.shadow.core:common:$shadow_version"
    compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口

Cree un nuevo SampleComponentManager, recuerde modificar el nombre del paquete de ruta de las tres actividades

import android.content.ComponentName;
import android.content.Context;

import com.tencent.shadow.core.loader.infos.ContainerProviderInfo;
import com.tencent.shadow.core.loader.managers.ComponentManager;

public class SampleComponentManager extends ComponentManager {
    
    

    /**
     * runtime 模块中定义的壳子Activity, 路径类名保持一致,需要在宿主AndroidManifest.xml注册
     */
    private static final String DEFAULT_ACTIVITY = "com.casic.titan.plugin_runtime.PluginDefaultProxyActivity";
    private static final String SINGLE_INSTANCE_ACTIVITY = "com.casic.titan.plugin_runtime.PluginSingleInstance1ProxyActivity";
    private static final String SINGLE_TASK_ACTIVITY = "com.casic.titan.plugin_runtime.PluginSingleTask1ProxyActivity";

    private Context context;

    public SampleComponentManager(Context context) {
    
    
        this.context = context;
    }


    /**
     * 配置插件Activity 到 壳子Activity的对应关系
     *
     * @param pluginActivity 插件Activity
     * @return 壳子Activity
     */
    @Override
    public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
    
    
        switch (pluginActivity.getClassName()) {
    
    
            /**
             * 这里配置对应的对应关系, 启动不同启动模式的Acitvity
             */
        }
        return new ComponentName(context, DEFAULT_ACTIVITY);
    }

    /**
     * 配置对应宿主中预注册的壳子contentProvider的信息
     */
    @Override
    public ContainerProviderInfo onBindContainerContentProvider(ComponentName pluginContentProvider) {
    
    
        return new ContainerProviderInfo(
                "com.tencent.shadow.core.runtime.container.PluginContainerContentProvider",
                context.getPackageName() + ".contentprovider.authority.dynamic");
    }

Nuevo SamplePluginLoader

public class SamplePluginLoader extends ShadowPluginLoader {
    
    

    private final static String TAG = "shadow";

    private ComponentManager componentManager;

    public SamplePluginLoader(Context hostAppContext) {
    
    
        super(hostAppContext);
        componentManager = new SampleComponentManager(hostAppContext);
    }

    @Override
    public ComponentManager getComponentManager() {
    
    
        return componentManager;
    }
}

Crear un nuevo directorio: Este directorio debe ser este, y no puede estar equivocado o cambiado

com.tencent.shadow.dynamic.loader.impl

Cree una nueva clase CoreLoaderFactoryImpl, el nombre de la clase no se puede cambiar

import android.content.Context;

import com.casic.titan.plugin_loader.SamplePluginLoader;
import com.tencent.shadow.core.loader.ShadowPluginLoader;

import org.jetbrains.annotations.NotNull;

/**
 * 这个类的包名类名是固定的。
 * <p>
 * 见com.tencent.shadow.dynamic.loader.impl.DynamicPluginLoader#CORE_LOADER_FACTORY_IMPL_NAME
 */
public class CoreLoaderFactoryImpl implements CoreLoaderFactory {
    
    

    @NotNull
    @Override
    public ShadowPluginLoader build(@NotNull Context context) {
    
    
        return new SamplePluginLoader(context);
    }
}

Hay WhiteList en la demostración oficial, puede usarlo cuando lo necesite

4.5 Implementación del administrador de complementos

Modificar las dependencias de build.gradle

    implementation project(path: ":constant")
    implementation "com.tencent.shadow.dynamic:manager:$shadow_version"
    compileOnly "com.tencent.shadow.core:common:$shadow_version"
    compileOnly "com.tencent.shadow.dynamic:host:$shadow_version"

Nuevo FastPluginManager


import android.content.Context;
import android.os.RemoteException;
import android.util.Pair;

import com.tencent.shadow.core.common.Logger;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.core.manager.installplugin.InstalledPlugin;
import com.tencent.shadow.core.manager.installplugin.InstalledType;
import com.tencent.shadow.core.manager.installplugin.PluginConfig;
import com.tencent.shadow.dynamic.host.FailedException;
import com.tencent.shadow.dynamic.manager.PluginManagerThatUseDynamicLoader;

import org.json.JSONException;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public abstract class FastPluginManager extends PluginManagerThatUseDynamicLoader {
    
    

    private static final Logger mLogger = LoggerFactory.getLogger(FastPluginManager.class);

    private ExecutorService mFixedPool = Executors.newFixedThreadPool(4);

    public FastPluginManager(Context context) {
    
    
        super(context);
    }


    public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException {
    
    
        final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
        final String uuid = pluginConfig.UUID;
        List<Future> futures = new LinkedList<>();
        List<Future<Pair<String, String>>> extractSoFutures = new LinkedList<>();
        if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
    
    
            Future odexRuntime = mFixedPool.submit(new Callable() {
    
    
                @Override
                public Object call() throws Exception {
    
    
                    oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,
                            pluginConfig.runTime.file);
                    return null;
                }
            });
            futures.add(odexRuntime);
            Future odexLoader = mFixedPool.submit(new Callable() {
    
    
                @Override
                public Object call() throws Exception {
    
    
                    oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,
                            pluginConfig.pluginLoader.file);
                    return null;
                }
            });
            futures.add(odexLoader);
        }
        for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
    
    
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;
            Future<Pair<String, String>> extractSo = mFixedPool.submit(() -> extractSo(uuid, partKey, apkFile));
            futures.add(extractSo);
            extractSoFutures.add(extractSo);
            if (odex) {
    
    
                Future odexPlugin = mFixedPool.submit(new Callable() {
    
    
                    @Override
                    public Object call() throws Exception {
    
    
                        oDexPlugin(uuid, partKey, apkFile);
                        return null;
                    }
                });
                futures.add(odexPlugin);
            }
        }

        for (Future future : futures) {
    
    
            future.get();
        }
        Map<String, String> soDirMap = new HashMap<>();
        for (Future<Pair<String, String>> future : extractSoFutures) {
    
    
            Pair<String, String> pair = future.get();
            soDirMap.put(pair.first, pair.second);
        }
        onInstallCompleted(pluginConfig, soDirMap);

        return getInstalledPlugins(1).get(0);
    }


    protected void callApplicationOnCreate(String partKey) throws RemoteException {
    
    
        Map map = mPluginLoader.getLoadedPlugin();
        Boolean isCall = (Boolean) map.get(partKey);
        if (isCall == null || !isCall) {
    
    
            mPluginLoader.callApplicationOnCreate(partKey);
        }
    }

    private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
    
    
        if (mPpsController == null) {
    
    
            bindPluginProcessService(getPluginProcessServiceName(partKey));
            waitServiceConnected(10, TimeUnit.SECONDS);
        }
        loadRunTime(uuid);
        loadPluginLoader(uuid);
    }

    protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
    
    
        loadPluginLoaderAndRuntime(uuid, partKey);
        Map map = mPluginLoader.getLoadedPlugin();
        if (!map.containsKey(partKey)) {
    
    
            mPluginLoader.loadPlugin(partKey);
        }
    }

    protected abstract String getPluginProcessServiceName(String partKey);

}

Nueva clase SamplePluginManager


public class SamplePluginManager extends FastPluginManager {
    
    
    private static final Logger logger = LoggerFactory.getLogger(SamplePluginManager.class);

    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    private Context mCurrentContext;

    public SamplePluginManager(Context context) {
    
    
        super(context);
        mCurrentContext = context;
    }

    /**
     * @return PluginManager实现的别名,用于区分不同PluginManager实现的数据存储路径
     */
    @Override
    protected String getName() {
    
    
        return "test-dynamic-manager";
    }

    /**
     * @return 宿主中注册的PluginProcessService实现的类名
     */
    @Override
    protected String getPluginProcessServiceName(String partKey) {
    
    
        if (PART_KEY_PLUGIN_USER.equals(partKey)) {
    
    
            return "com.casic.titan.shadow.plugin_manager.UserPluginProcessService";
        } else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {
    
    
            return "com.casic.titan.shadow.plugin_manager.MainPluginProcessService";
        }
        throw new RuntimeException("partKey is unknown");
    }

    @Override
    public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
    
    
        if (fromId == Constant.FROM_ID_NOOP) {
    
    
            //do nothing.
        } else if (fromId == Constant.FROM_ID_START_ACTIVITY) {
    
    
            onStartActivity(context, bundle, callback);
        } else if (fromId == Constant.FROM_ID_CLOSE) {
    
    
            close();
        } else if (fromId == Constant.FROM_ID_LOAD_VIEW_TO_HOST) {
    
    
            loadViewToHost(context, bundle);
        } else {
    
    
            throw new IllegalArgumentException("不认识的fromId==" + fromId);
        }
    }

    private void loadViewToHost(final Context context, Bundle bundle) {
    
    
        Intent pluginIntent = new Intent();
        pluginIntent.setClassName(
                context.getPackageName(),
                "com.tencent.shadow.sample.plugin.app.lib.usecases.service.HostAddPluginViewService"
        );
        pluginIntent.putExtras(bundle);
        try {
    
    
            mPluginLoader.startPluginService(pluginIntent);
        } catch (RemoteException e) {
    
    
            throw new RuntimeException(e);
        }
    }

    private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
    
    
        final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
        final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
        final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
        logger.debug("pluginZipPath:"+pluginZipPath);
        logger.debug("className:"+className);
        if (className == null) {
    
    
            throw new NullPointerException("className == null");
        }
        final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);

        if (callback != null) {
    
    
            final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
            callback.onShowLoadingView(view);
        }

        executorService.execute(() -> {
    
    
            try {
    
    
                InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);

                loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);
                callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                callApplicationOnCreate(PART_KEY_PLUGIN_USER);

                Intent pluginIntent = new Intent();
                pluginIntent.setClassName(
                        context.getPackageName(),
                        className
                );
                if (extras != null) {
    
    
                    pluginIntent.replaceExtras(extras);
                }
                Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                mPluginLoader.startActivityInPluginProcess(intent);
            } catch (Exception e) {
    
    
                throw new RuntimeException(e);
            }
            if (callback != null) {
    
    
                callback.onCloseLoadingView();
            }
        });
    }
}

Modifique el código de la clase SamplePluginManager anterior:

    /**
     * @return 宿主中注册的PluginProcessService实现的类名
     */
    @Override
    protected String getPluginProcessServiceName(String partKey) {
    
    
        if (PART_KEY_PLUGIN_USER.equals(partKey)) {
    
    //与你partKey和Service对应起来,修改为自己的报名和静态参数
            return "com.casic.titan.shadow.plugin_manager.UserPluginProcessService";
        } else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {
    
    //与你partKey和Service对应起来,修改为自己的报名和静态参数
            return "com.casic.titan.shadow.plugin_manager.MainPluginProcessService";
        }
        throw new RuntimeException("partKey is unknown");
    }

Modificar el método de clase SamplePluginManager onStartActivity

//两个插件这样写
                loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);
                callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                callApplicationOnCreate(PART_KEY_PLUGIN_USER);
  //一个插件就删掉一个,这样写
                  loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                callApplicationOnCreate(PART_KEY_PLUGIN_BASE);

Nuevo nombre del paquete: no se puede modificar y no puede estar equivocado

com.tencent.shadow.dynamic.impl

Recién creado bajo la ruta: ManagerFactoryImpl, el tipo no se puede modificar

import android.content.Context;

import com.casic.titan.plugin_manager.SamplePluginManager;
import com.tencent.shadow.dynamic.host.ManagerFactory;
import com.tencent.shadow.dynamic.host.PluginManagerImpl;

/**
 * 此类包名及类名固定
 */
public final class ManagerFactoryImpl implements ManagerFactory {
    
    
    @Override
    public PluginManagerImpl buildManager(Context context) {
    
    
        return new SamplePluginManager(context);
    }
}

4.6, implementación del complemento en tiempo de ejecución

Agregar dependencias

    implementation "com.tencent.shadow.core:activity-container:$shadow_version"
	//其他的包可以删除,当然你删除后也一定要删除同步的res和清单文件不然会报错,也可以不删

Cree tres nuevas actividades sin registrarse en el archivo de manifiesto. Esta es la actividad registrada en el archivo de manifiesto del host. De hecho, esto se usa para cargar la actividad en su complemento, porque la sombra casi reescribe el código de actividad.

import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginDefaultProxyActivity extends PluginContainerActivity {
    
    
}
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity {
    
    
}
import com.tencent.shadow.core.runtime.container.PluginContainerActivity;
public class PluginSingleTask1ProxyActivity extends PluginContainerActivity {
    
    
}

Agregue el paquete al archivo de manifiesto, porque Android Studio no agregará este paquete al crear un nuevo proyecto y módulo, por lo que debe agregarlo manualmente

package="com.casic.titan.plugin_runtime"

archivo de manifiesto completo

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.casic.titan.plugin_runtime">


</manifest>

4.7 Implementación de proyectos de complemento

Aquí escribiré uno, el otro es el mismo pero el código es diferente
Agregar dependencias:

    compileOnly "com.tencent.shadow.core:runtime:$shadow_version"

Agregar debajo del nodo de Android

    buildTypes {
    
    
        debug {
    
    
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        release {
    
    
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

            signingConfig signingConfigs.create("release")
            signingConfig.initWith(buildTypes.debug.signingConfig)
        }
    }
    compileOptions {
    
    
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    lintOptions {
    
    
        abortOnError false
    }
    // 将插件applicationId设置为和宿主相同
    productFlavors {
    
    
        plugin {
    
    
            applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
        }
    }
    // 将插件的资源ID分区改为和宿主0x7F不同的值
    aaptOptions {
    
    
        additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
    }

Agregue en la parte superior de build.gradle, asegúrese de escribir en la parte superior

buildscript {
    
    //这个模块要放在plugins {} 之前
    repositories {
    
    
        maven {
    
     setUrl("https://localhost:9224/repository/casic_group/") }
        maven {
    
     setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
        google()
        mavenLocal()
        mavenCentral()
    }

    dependencies {
    
    
        classpath "com.tencent.shadow.core:runtime:$shadow_version"
        classpath "com.tencent.shadow.core:activity-container:$shadow_version"
        classpath "com.tencent.shadow.core:gradle-plugin:$shadow_version"
        classpath "org.javassist:javassist:$javassist_version"
    }
}


apply plugin: 'com.android.application'
apply plugin: 'com.tencent.shadow.plugin'

En la parte inferior de build.gradle, agregue el código en la parte inferior, solo agréguelo con un complemento, en lugar de agregar cada proyecto de complemento, porque el código aquí es solo para pruebas. La configuración de dependencia anterior se agrega a cada proyecto de complemento, solo que aquí no se usa, porque es prueba, prueba, prueba

shadow {
    
    
    transform {
    
    
//   useHostContext = ['abc']
    }

    packagePlugin {
    
    
        pluginTypes {
    
    
            debug {
    
    
                loaderApkConfig = new Tuple2('plugin-loader-debug.apk', ':plugin-loader:assembleDebug')//其中plugin-loader是moduel的名字plugin-loader
                runtimeApkConfig = new Tuple2('plugin-runtime-debug.apk', ':plugin-runtime:assembleDebug')
                pluginApks {
    
    
                    pluginApp {
    
    
                        businessName = 'plugin-app'//与下面保持一致最好,也可以为空,具体解释可以看官方的
                        partKey = 'plugin-app'//静态参数constant 的module中的partKey,我建议和项目名保持一致
                        buildTask = ':plugin-app:assemblePluginDebug'//这里的plugin-app是module名字,因为需要调用脚本assemblePluginDebug,这句话是意思是加载plugin-app下的assemblePluginDebug任务
                        apkPath = 'plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk'//编译后apk的输出位置
                    }
                    pluginUser {
    
    //第二个插件
                        businessName = 'plugin-user'//与下面保持一致最好,也可以为空,具体解释可以看官方的
                        partKey = 'plugin-user'//静态参数constant 的module中的partKey
                        buildTask = ':plugin-user:assemblePluginDebug'//这里的plugin-app是module名字,因为需要调用脚本assemblePluginDebug,这句话是意思是加载plugin-app下的assemblePluginDebug任务
                        apkPath = 'plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk'//编译后apk的输出位置
                    }
                }
            }

            release {
    
    //这个就不解释了,是版本的区别
                loaderApkConfig = new Tuple2('plugin-loader-release.apk', ':plugin-loader:assembleRelease')
                runtimeApkConfig = new Tuple2('plugin-runtime-release.apk', ':plugin-runtime:assembleRelease')
                pluginApks {
    
    
                    pluginApp {
    
    
                        businessName = 'plugin-app'
                        partKey = 'plugin-app'
                        buildTask = ':plugin-app:assemblePluginRelease'
                        apkPath = 'plugin-app/build/outputs/apk/release/plugin-app-release.apk'
                    }
                    pluginUser {
    
    
                        businessName = 'plugin-user'
                        partKey = 'plugin-user'
                        buildTask = ':plugin-user:assemblePluginRelease'
                        apkPath = 'plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk'
                    }
                }
            }
        }

        loaderApkProjectPath = 'plugin-loader'
        runtimeApkProjectPath = 'plugin-runtime'

        archiveSuffix = System.getenv("PluginSuffix") ?: ""
        archivePrefix = 'plugin'
        destinationDir = "${getRootProject().getBuildDir()}"

        version = 1//暂时不知道干嘛的,等我阅读下源码再说
        compactVersion = [1, 2, 3]
        uuidNickName = "1.0"
    }
}

Recuerde agregar el paquete en el archivo de manifiesto, asegúrese de agregar

package="com.casic.titan.shadow"

No hay nada más que decir, escribe una actividad para demostración:

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:text="这是plugin-app插件内容哦!!!"
        android:textSize="16sp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

El complemento está listo. Por supuesto, es imposible escribir una lógica comercial tan pequeña al escribir un proyecto. Implementémoslo de acuerdo con la nuestra.

4.8 El anfitrión llama a la actividad del complemento

Cree un nuevo archivo de diseño en el proyecto host, activé el enlace de datos, por lo que el diseño es diferente, no necesito explicarlo

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:id="@+id/start_plugin_app"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="16dp"
            android:textAllCaps="false"
            android:onClick="start_plugin_app"
            android:text="启动插件plugin-app" />

        <Button
            android:id="@+id/start_plugin_user"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/start_plugin_app"
            android:onClick="start_plugin_user"
            android:textAllCaps="false"
            android:text="启动插件plugin-user" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Cree una nueva MainActivity y configúrela como la página de inicio, haga clic en el código del evento:

public void start_plugin_app(View view) {
    
    
        PluginHelper.getInstance().singlePool.execute(()-> {
    
    
            MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);

            /**
             * @param context context
             * @param formId  标识本次请求的来源位置,用于区分入口
             * @param bundle  参数列表, 建议在参数列表加入自己的验证
             * @param callback 用于从PluginManager实现中返回View
             */
            Bundle bundle = new Bundle();//插件 zip,这几个参数也都可以不传,直接在 PluginManager 中硬编码
            bundle.putString(
                    Constant.KEY_PLUGIN_ZIP_PATH,
                    PluginHelper.getInstance().pluginZipFile.getAbsolutePath()
            );
            bundle.putString(
                    Constant.KEY_PLUGIN_NAME,
                    Constant.PLUGIN_APP_NAME
            ); // partKey 每个插件都有自己的 partKey 用来区分多个插件,如何配置在下面讲到
            bundle.putString(
                    Constant.KEY_ACTIVITY_CLASSNAME,
                    "com.casic.titan.shadow.MainActivity"
            ); //要启动的插件的Activity页面
            bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数
            MyApplication.getApp().getPluginManager().enter(
                    this,
                    Constant.FROM_ID_START_ACTIVITY,
                    bundle,
                    new EnterCallback() {
    
    
                        @Override
                        public void onShowLoadingView(View view) {
    
    

                        }

                        @Override
                        public void onCloseLoadingView() {
    
    

                        }

                        @Override
                        public void onEnterComplete() {
    
    

                        }
                    });
        });
    }

Inicie el código de evento de clic del complemento de usuario del complemento

public void start_plugin_user(View view) {
    
    
        PluginHelper.getInstance().singlePool.execute(()-> {
    
    
            MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);

            /**
             * @param context context
             * @param formId  标识本次请求的来源位置,用于区分入口
             * @param bundle  参数列表, 建议在参数列表加入自己的验证
             * @param callback 用于从PluginManager实现中返回View
             */
            Bundle bundle = new Bundle();//插件 zip,这几个参数也都可以不传,直接在 PluginManager 中硬编码
            bundle.putString(
                    Constant.KEY_PLUGIN_ZIP_PATH,
                    PluginHelper.getInstance().pluginZipFile.getAbsolutePath()
            );
            bundle.putString(
                    Constant.KEY_PLUGIN_NAME,
                    Constant.PLUGIN_USER_NAME
            ); // partKey 每个插件都有自己的 partKey 用来区分多个插件,如何配置在下面讲到
            bundle.putString(
                    Constant.KEY_ACTIVITY_CLASSNAME,
                    "com.casic.titan.user.LoginActivity"//插件中的Activity全路径
            ); //要启动的插件的Activity页面
            bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数
            MyApplication.getApp().getPluginManager().enter(
                    this,
                    Constant.FROM_ID_START_ACTIVITY,
                    bundle,
                    new EnterCallback() {
    
    
                        @Override
                        public void onShowLoadingView(View view) {
    
    

                        }

                        @Override
                        public void onCloseLoadingView() {
    
    

                        }

                        @Override
                        public void onEnterComplete() {
    
    

                        }
                    });
        });
    }

4.9, iniciar el proyecto

Antes de iniciar el proyecto, el complemento debe empaquetarse y el código se agrega en la parte inferior de build.gradle debajo del módulo host


def createCopyTask(projectName, buildType, name, apkName, inputFile, taskName) {
    
    
    def outputFile = file("${getBuildDir()}/generated/assets/${name}/${buildType}/${apkName}")
    outputFile.getParentFile().mkdirs()
    return tasks.create("copy${buildType.capitalize()}${name.capitalize()}Task", Copy) {
    
    
        group = 'build'
        description = "复制${name}到assets中."
        from(inputFile.getParent()) {
    
    
            include(inputFile.name)
            rename {
    
     outputFile.name }
        }
        into(outputFile.getParent())
    }.dependsOn("${projectName}:${taskName}")
}

def generateAssets(generateAssetsTask, buildType) {
    
    

    def moduleName = 'plugin-manager'
    def pluginManagerApkFile = file(
            "${project(":plugin-manager").getBuildDir()}" +
                    "/outputs/apk/${buildType}/" +
                    "${moduleName}-${buildType}.apk"
    )
    generateAssetsTask.dependsOn createCopyTask(
            ':plugin-manager',//项目名
            buildType,
            moduleName,
            'plugin-manager.apk',//plugin-manager   module打包后的apk名字,我建议是module名保持一致,此出名字要和宿主类PluginHelper中的sPluginManagerName参数保持一致
            pluginManagerApkFile,
            "assemble${buildType.capitalize()}"
    )

    def pluginZip = file("${getRootProject().getBuildDir()}/plugin-${buildType}.zip")
    generateAssetsTask.dependsOn createCopyTask(
            ':plugin-app',//这里至于为什么写plugin-app具体暂时还没摸明白,反正这么写可以等我再研究下源码
            buildType,
            'plugin-zip',
            "plugin-${buildType}.zip",
            pluginZip,
            "package${buildType.capitalize()}Plugin"
    )
}

tasks.whenTaskAdded {
    
     task ->
    if (task.name == "generateDebugAssets") {
    
    
        generateAssets(task, 'debug')
    }
    if (task.name == "generateReleaseAssets") {
    
    
        generateAssets(task, 'release')
    }
}

4.10, corre para ver el nuevo efecto

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

Cinco, pisando el pozo

5.1 El paquete debe agregarse al archivo de manifiesto del proyecto de complemento

5.2 Se debe hacer referencia a los complementos en build.gradle de cada proyecto de complemento

apply plugin: 'com.tencent.shadow.plugin'

Y agregue classpath, en la parte superior. Por supuesto, si empaqueta la aplicación por separado, estas configuraciones deben estar en build.gradle debajo del proyecto. Depende de si el proyecto del complemento se desarrolla por separado o bajo el host del complemento. proyecto.

buildscript {
    
    //这个模块要放在plugins {} 
    repositories {
    
    
    //换成你自己的module
        maven {
    
     setUrl("https://localhost:9224/repository/casic_group/") }
        maven {
    
     setUrl("https://mirrors.tencent.com/nexus/repository/maven-public/") }
        google()
        mavenLocal()
        mavenCentral()
    }

    dependencies {
    
    
        classpath "com.tencent.shadow.core:runtime:$shadow_version"
        classpath "com.tencent.shadow.core:activity-container:$shadow_version"
        classpath "com.tencent.shadow.core:gradle-plugin:$shadow_version"
        classpath "org.javassist:javassist:$javassist_version"
    }
}

5.3, cada complemento necesita agregar dependencias

compileOnly "com.tencent.shadow.core:runtime:$shadow_version"

5.4 Preste atención a la recomendación oficial de la versión gradle

plugins {
    
    
    id 'com.android.application' version '7.0.3' apply false
}
#Thu Aug 03 17:23:27 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

5.5 Preste atención a modificar el número de versión en cada build.gradle

Por ejemplo, dependiendo de la versión de la biblioteca, unifíquela para evitar que se informe un error si cierta versión es demasiado alta y no puede encontrar el problema durante mucho tiempo.

    implementation "androidx.appcompat:appcompat:$appcompatVersion"
    implementation "com.google.android.material:material:$materialVersion"
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion"

minSdk project.MIN_SDK_VERSION
        targetSdk project.TARGET_SDK_VERSION

5.6 Preste atención a modificar los nombres de los paquetes que no se mencionan en mi artículo

com.casic.titan.shadow  //这是我宿主的包名

5.7 Recomendaciones oficiales

La sugerencia oficial es que el nombre del paquete del complemento debe ser el mismo que el nombre del paquete del host, pero todos saben que esto no es práctico, así que escribí dos complementos, uno es el mismo que el del host y el otro es diferente. El método oficial para resolver el problema está en el nodo de Android en el complemento build.gradle agregar configuración

    // 将插件applicationId设置为和宿主相同
    productFlavors {
    
    
        plugin {
    
    
            applicationId project.SAMPLE_HOST_APP_APPLICATION_ID
        }
    }

5.8, varios complementos necesitan crear un nuevo servicio y ya no pueden unificar el proceso

 <service
            android:name=".plugin_manager.MainPluginProcessService"
            android:exported="true"
            android:process=":plugin" />
        <service
            android:name=".plugin_manager.UserPluginProcessService"
            android:exported="true"
            android:process=":plugin_user" />

5.9 Preste atención a que el nombre del paquete no sea incorrecto, trate de mantener la clave de la pieza y el nombre del módulo consistentes, lo que puede reducir la tasa de tolerancia a fallas

5.10 Presta atención a cada código de comentario en el texto

Seis, escribe al final

Si tiene alguna pregunta, deje un mensaje en el blog y también puede plantear un problema en github. Le responderé lo antes posible.

Supongo que te gusta

Origin blog.csdn.net/fzkf9225/article/details/132107432
Recomendado
Clasificación