Guía de acceso al complemento Android Tencent Shadow
- Breve introducción del complemento
-
- 1. almacén de clones
- 2. Compile y ejecute la demostración oficial
- 3. Publique Shadow en nuestro almacén local
- 4. Escribir nuestro propio código
-
- 4.1 Crear un nuevo proyecto e importar configuraciones comunes como maven
- 4.2 Realizar el módulo host
- 4.3 Módulo de escritura de constante de parámetro estático
- 4.4, implementación del módulo cargador de complementos
- 4.5 Implementación del administrador de complementos
- 4.6, implementación del complemento en tiempo de ejecución
- 4.7 Implementación de proyectos de complemento
- 4.8 El anfitrión llama a la actividad del complemento
- 4.9, iniciar el proyecto
- 4.10, corre para ver el nuevo efecto
- 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
- 5.3, cada complemento necesita agregar dependencias
- 5.4 Preste atención a la recomendación oficial de la versión gradle
- 5.5 Preste atención a modificar el número de versión en cada build.gradle
- 5.6 Preste atención a modificar los nombres de los paquetes que no se mencionan en mi artículo
- 5.7 Recomendaciones oficiales
- 5.8, varios complementos necesitan crear un nuevo servicio y ya no pueden unificar el proceso
- 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
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
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
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):
- 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
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.