报错了也不管(因为有的错误,确实不影响结果,最终我们需要的是plugin-debug.zip(插件apk的产出物)和plugin-manager.apk(plugin-manager module的产出物)),一眼走到黑。
- 导入
-
直接导入Shadow的SDK到项目的根目录
-
直接导入Shadow的buildScripts到项目的根目录
-
在settings.gradle添加:
includeBuild 'sdk/coding' includeBuild 'sdk/core' includeBuild 'sdk/dynamic'
-
在project/build.gradle添加配置如下:
apply from: 'buildScripts/gradle/common.gradle' apply from: "buildScripts/gradle/maven.gradle" task clean(type: Delete) {//定义任务 delete rootProject.buildDir dependsOn gradle.includedBuild('coding').task(':checks:clean')//添加依赖 dependsOn gradle.includedBuild('coding').task(':lint:clean') dependsOn gradle.includedBuild('coding').task(':code-generator:clean') dependsOn gradle.includedBuild('core').task(':gradle-plugin:clean') dependsOn gradle.includedBuild('core').task(':common:clean') dependsOn gradle.includedBuild('core').task(':loader:clean') dependsOn gradle.includedBuild('core').task(':manager:clean') dependsOn gradle.includedBuild('core').task(':runtime:clean') dependsOn gradle.includedBuild('core').task(':activity-container:clean') dependsOn gradle.includedBuild('core').task(':transform:clean') dependsOn gradle.includedBuild('core').task(':transform-kit:clean') dependsOn gradle.includedBuild('dynamic').task(':dynamic-host:clean') dependsOn gradle.includedBuild('dynamic').task(':dynamic-loader:clean') dependsOn gradle.includedBuild('dynamic').task(':dynamic-loader-impl:clean') dependsOn gradle.includedBuild('dynamic').task(':dynamic-manager:clean') }
-
好了,build一下。
如果报错:
方案1:修改所有和buildScripts
相关的路径(麻烦)。
方案2:直接把buildScripts
copy到所示的目录(方便)。
用来各个module间使用相同的变量,用到的module:plugin-host和plugin-manager。
final public class Constant {
public static final String KEY_PLUGIN_ZIP_PATH = "key_plugin_zip_path";
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 PLUGIN_APP_NAME = "plugin-app";
public static final String PLUGIN_OTHER_NAME = "plugin-other";
public static final String KEY_PLUGIN_PART_KEY = "KEY_PLUGIN_PART_KEY";
public static final String PART_KEY_PLUGIN_MAIN_APP = "sample-plugin-app";
public static final String PART_KEY_PLUGIN_ANOTHER_APP = "sample-plugin-app2";
public static final String PART_KEY_PLUGIN_BASE = "plugin-app"; //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;
}
(宿主App 就是大的APP,大APP接小APP)
-
在app/build.gradle添加:
android { 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/') } } } dependencies { implementation project(path: ':constants') implementation 'com.tencent.shadow.core:common'//AndroidLogLoggerFactory implementation 'commons-io:commons-io:2.9.0'//sample-host从assets中复制插件用的 implementation 'com.tencent.shadow.dynamic:dynamic-host'//腾讯插件框架shadow }
-
添加代理 Activity 主题:
在app/values/themes.xml声明:
<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>
-
清单文件注册代理Activity:
单文件中 预先 添加runtime lib的activity:
注意:这里声明的Activity的包名要和待会儿创建的plugin-runtime module包名保持一致 。Service:
MainPluginProcessService
路径和在宿主App中声明的保持一直即可。//android:multiprocess="true"//表示多进程(多插件的时候使用) //android:process=":plugin"//表示单进程(当个插件使用) //android:theme="@style/transparent_theme" //theme需要注册成透明 //<!--以下这些类不打包在宿主app中,打包在plugin-runtime中,以便减少宿主方法数增量,Activity 路径需要和插件中的匹配,后面会说到--> <!--container 注册 注意configChanges需要全注册 theme需要注册成透明 这些类不打包在host中,打包在runtime中,以便减少宿主方法数增量 Activity 路径需要和插件中的匹配,后面会说到 --> <activity android:name="com.lph.plugin_host.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:process=":plugin" /> <activity android:name="com.lph.plugin_host.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:theme="@style/PluginContainerActivity" android:process=":plugin" /> <activity android:name="com.lph.plugin_host.runtime.PluginSingleTask1ProxyActivity" android:launchMode="singleTask" 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:process=":plugin" /> <provider android:authorities="com.tencent.shadow.contentprovider.authority.dynamic" android:name="com.tencent.shadow.core.runtime.container.PluginContainerContentProvider" /> <!--container 注册 end --> <service android:name=".plugin_manager.MainPluginProcessService" android:process=":plugin" />
-
在宿主中创建 PluginManager 管理工具:
PluginManager 是用来装载插件,PluginManager 通过加载一个单独的apk来创建的。
下面会说怎么生成这个apk,先知道在宿主中怎么用。
4.1 创建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;
}
}
4.2 创建插件进程服务:
import com.tencent.shadow.dynamic.host.PluginProcessService;
/**
* 一个PluginProcessService(简称PPS)代表一个插件进程。插件进程由PPS启动触发启动。
* 新建PPS子类允许一个宿主中有多个互不影响的插件进程。
*/
public class MainPluginProcessService extends PluginProcessService {
}
在清单文件注册:
<service
android:name=".MainPluginProcessService"
android:process=":plugin" />
4.3 实现Log工具:
AndroidLoggerFactory.java
4.4 创建 PluginHelper
:
import android.content.Context;
import android.os.Environment;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static com.lph.plugin_host.BuildConfig.DEBUG;
public class PluginHelper {
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);
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);
}
}
}
4.5 创建 Shadow 类:
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;
}
}
4.6 在 Application 创建 PluginManager:
import android.app.ActivityManager;
import android.app.Application ;
import android.content.Context;
import android.os.Build;
import android.os.StrictMode;
import android.webkit.WebView;
import com.lph.plugin_host.plugin_manager.AndroidLoggerFactory;
import com.lph.plugin_host.plugin_manager.PluginHelper;
import com.lph.plugin_host.plugin_manager.Shadow;
import com.tencent.shadow.core.common.LoggerFactory;
import com.tencent.shadow.dynamic.host.DynamicRuntime;
import com.tencent.shadow.dynamic.host.PluginManager;
import java.io.File;
import java.util.concurrent.Future;
import static android.os.Process.myPid;
public class MyApplication extends Application {
private static MyApplication sApp;
private static PluginManager sPluginManager;//这个PluginManager对象在Manager升级前后是不变的。它内部持有具体实现,升级时更换具体实现
@Override
public void onCreate() {
super.onCreate();
sApp = this;
detectNonSdkApiUsageOnAndroidP();
setWebViewDataDirectorySuffix();
LoggerFactory.setILoggerFactory(new AndroidLoggerFactory());
if (isProcess(this, ":main_plugin")) {//TODO
//在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
//为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
//因此这里恢复加载上一次的runtime
DynamicRuntime.recoveryRuntime(this);
}
PluginHelper.getInstance().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;
}
boolean isRunningEspressoTest;
try {
Class.forName("androidx.test.espresso.Espresso");
isRunningEspressoTest = true;
} catch (Exception ignored) {
isRunningEspressoTest = false;
}
if (isRunningEspressoTest) {
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 (sPluginManager == null) {
sPluginManager = Shadow.getPluginManager(apk);
}
}
public PluginManager getPluginManager() {
return sPluginManager;
}
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);
}
-
宿主APP 启动插件 Activity:
这里需要注意的是: Constant.KEY_ACTIVITY_CLASSNAME, "com.lph.plugin_host.MainActivity" 插件 activity的完整包名
布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="start_plugin" android:text="启动插件" /> <LinearLayout android:id="@+id/ll" android:layout_width="match_parent" android:layout_height="300dp" android:layout_marginTop="20dp" android:orientation="horizontal" /> </LinearLayout>
package com.lph.plugin_host import android.os.Bundle import android.os.Handler import android.util.Log import android.view.View import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import com.lph.constants.Constant import com.lph.plugin_host.base.MyApplication import com.lph.plugin_host.plugin_manager.PluginHelper import com.tencent.shadow.dynamic.host.EnterCallback import org.jetbrains.annotations.Nullable class MainActivity : AppCompatActivity() { private var ll: LinearLayout? = null private val mHandler: Handler = Handler() override fun onCreate(@Nullable savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ll = findViewById<LinearLayout>(R.id.ll) } fun start_plugin(view: View?) { PluginHelper.getInstance().singlePool.execute(Runnable { MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile) /** * @param context context * @param formId 标识本次请求的来源位置,用于区分入口 * @param bundle 参数列表, 建议在参数列表加入自己的验证 * @param callback 用于从PluginManager实现中返回View */ val bundle = 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.lph.plugin_host.MainActivity" ) //要启动的插件的Activity页面 bundle.putBundle(Constant.KEY_EXTRAS, Bundle()) // 要传入到插件里的参数 MyApplication.getApp().getPluginManager().enter( this@MainActivity, Constant.FROM_ID_START_ACTIVITY, bundle, object : EnterCallback { override fun onShowLoadingView(view: View) { Log.e("PluginLoad", "onShowLoadingView") loading(view) //这里进行加载视图 } override fun onCloseLoadingView() { Log.e("PluginLoad", "onCloseLoadingView") } override fun onEnterComplete() { // 启动成功 Log.e("PluginLoad", "onEnterComplete") } }) }) } private fun loading(view: View) { mHandler.post(Runnable { ll!!.removeAllViews() ll!!.addView(view) }) } }
创建一个新 Module:plugin-manager (app类型的module)。
-
添加依赖:
dependencies { implementation project(path: ':constants') implementation 'com.tencent.shadow.dynamic:dynamic-manager' implementation 'com.tencent.shadow.core:manager' implementation 'com.tencent.shadow.dynamic:dynamic-loader' compileOnly 'com.tencent.shadow.core:common' compileOnly 'com.tencent.shadow.dynamic:dynamic-host' }
-
**创建插件管理类FastPluginManager:
/* * Tencent is pleased to support the open source community by making Tencent Shadow available. * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of * the License at * * https://opensource.org/licenses/BSD-3-Clause * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ 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); }
-
创建业务实习类
LphPluginManager
:需要注意的:getPluginProcessServiceName是在宿主app中声明的MainPluginProcessService类全路径包名
/* * Tencent is pleased to support the open source community by making Tencent Shadow available. * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of * the License at * * https://opensource.org/licenses/BSD-3-Clause * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import static com.lph.constants.Constant.PART_KEY_PLUGIN_BASE; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.RemoteException; import android.view.LayoutInflater; import android.view.View; import com.lph.constants.Constant; import com.tencent.shadow.core.manager.installplugin.InstalledPlugin; import com.tencent.shadow.dynamic.host.EnterCallback; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class LphPluginManager extends FastPluginManager { private ExecutorService executorService = Executors.newSingleThreadExecutor(); private Context mCurrentContext; public LphPluginManager(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) { return "com.lph.plugin_host.plugin_manager.MainPluginProcessService"; } @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); 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(new Runnable() { @Override public void run() { try { InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true); loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE); // loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP); callApplicationOnCreate(PART_KEY_PLUGIN_BASE); // callApplicationOnCreate(PART_KEY_PLUGIN_MAIN_APP); 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(); } } }); } }
-
activity_load_plugin.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="vertical" android:background="#fcfcfc"> <ProgressBar android:layout_width="40dp" android:layout_height="40dp" android:layout_gravity="center_horizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="正在启动插件" /> </LinearLayout>
-
使用插件管理类
需要使用下面的路径和类名:
- 两个类的路径一定是:com.tencent.shadow.dynamic.impl
- 类名:ManagerFactoryImpl 和WhiteList
/* * Tencent is pleased to support the open source community by making Tencent Shadow available. * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of * the License at * * https://opensource.org/licenses/BSD-3-Clause * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.tencent.shadow.dynamic.impl; import android.content.Context; import com.lph.plugin_manager.LphPluginManager; 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 LphPluginManager(context); } }
/* * Tencent is pleased to support the open source community by making Tencent Shadow available. * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of * the License at * * https://opensource.org/licenses/BSD-3-Clause * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.tencent.shadow.dynamic.impl; /** * 此类包名及类名固定 * classLoader的白名单 * PluginManager可以加载宿主中位于白名单内的类 */ public interface WhiteList { String[] sWhiteList = new String[] { "com.tencent.host.shadow", "com.tencent.shadow.test.lib.constant", }; }
-
编译获取 manager apk:
./gradlew assembleDebug
****创建应用级模块 plugin-runtime****
-
添加依赖 “plugin-runtime”应用模块主要放在宿主中注册的壳子,这个模块的 applicationd 可以随意。
这个应用模块只需要下面的依赖,其他依赖都能删掉; 清单文件也不需要任何配置; 生成项目时自动创建的Activity 都可以删掉。
dependencies { implementation 'com.tencent.shadow.core:activity-container' }
-
清单文件:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lph.plugin_host"> </manifest>
-
完成之前在宿主APP中声明的3个Activity的空实现:
壳子路径包名要和宿主中注册的保持一致:
PluginDefaultProxyActivity
:import com.tencent.shadow.core.runtime.container.PluginContainerActivity; public class PluginDefaultProxyActivity extends PluginContainerActivity { }
PluginSingleInstance1ProxyActivity
:import com.tencent.shadow.core.runtime.container.PluginContainerActivity; public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity { }
PluginSingleTask1ProxyActivity
:public class PluginSingleTask1ProxyActivity extends PluginContainerActivity { }
检查下宿主app/AndroidManifest.xml,配置的activity壳子路径是否一致。
创建应用级模块 plugin-loader
这个应用模块主要定义插件组件和壳子代理组件的配对关系。 PluginManager(插件管理器)在加载"插件"时,首先需要先加载"插件"中的runtime和loader,再通过loader的Binder(插件应该处于独立进程中避免native库冲突)操作loader进而加载业务App。 这个模块的applicationId 可以随意。
-
添加依赖
• 这个应用模块只需要下面的依赖,其他依赖都能删掉;
• 清单文件也不需要任何配置;
• 生成项目时自动创建的Activity 都可以删掉。
dependencies{ implementation 'com.tencent.shadow.core:loader' implementation 'com.tencent.shadow.dynamic:dynamic-loader' implementation 'com.tencent.shadow.dynamic:dynamic-loader-impl' compileOnly 'com.tencent.shadow.core:runtime' compileOnly 'com.tencent.shadow.core:activity-container' compileOnly 'com.tencent.shadow.core:common' compileOnly 'com.tencent.shadow.dynamic:dynamic-host'//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口 }
-
实现插件组件管理类
-
SampleComponentManager
需要注意的:runtime 模块中定义的壳子Activity, 路径类名保持一致,需要在宿主AndroidManifest.xml注册
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.lph.plugin_host.runtime.PluginDefaultProxyActivity"; private static final String SINGLE_INSTANCE_ACTIVITY = "com.lph.plugin_host.runtime.PluginSingleInstance1ProxyActivity"; private static final String SINGLE_TASK_ACTIVITY = "com.lph.plugin_host.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.runtime.container.PluginContainerContentProvider", "com.tencent.shadow.contentprovider.authority.dynamic"); } }
b.
SamplePluginLoader
:import android.content.Context; import com.tencent.shadow.core.loader.ShadowPluginLoader; import com.tencent.shadow.core.loader.managers.ComponentManager; 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; } }
-
-
使用插件加载器
- 类名:CoreLoaderFactoryImpl
- CoreLoaderFactoryImpl 的路径一定是:com.tencent.shadow.dynamic.loader.impl
package com.tencent.shadow.dynamic.loader.impl; import android.content.Context; import com.lph.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); } }
- 只要插件的包名和宿主的包名保持一致
- 业务代码:
<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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hahahhahahahh 跑起来 冲啊"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
-
设置build.gradle
buildscript {//这个模块要放在plugins {} 之前 repositories { google() jcenter() } dependencies { classpath 'com.tencent.shadow.core:runtime' classpath 'com.tencent.shadow.core:activity-container' classpath 'com.tencent.shadow.core:gradle-plugin' classpath 'org.javassist:javassist:3.22.0-GA' } } apply plugin: 'com.android.application' apply plugin: 'com.tencent.shadow.plugin' apply plugin: 'org.jetbrains.kotlin.android' android { namespace 'com.lph.plugin_host' compileSdk 33 defaultConfig { applicationId "com.lph.plugin_host" minSdk 24 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } aaptOptions { additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.6.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' //如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类 compileOnly 'com.tencent.shadow.core:runtime' } shadow { transform { // useHostContext = ['abc'] } packagePlugin { pluginTypes { debug { loaderApkConfig = new Tuple2('plugin-loader-debug.apk', ':plugin-loader:assembleDebug') runtimeApkConfig = new Tuple2('plugin-runtime-debug.apk', ':plugin-runtime:assembleDebug') pluginApks { pluginApk1 { businessName = 'plugin-app' partKey = 'plugin-app' buildTask = ':plugin-app:assembleDebug' apkPath = 'plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk' } } } release { loaderApkConfig = new Tuple2('plugin-loader-release.apk', ':plugin-loader:assembleRelease') runtimeApkConfig = new Tuple2('plugin-runtime-release.apk', ':plugin-runtime:assembleRelease') pluginApks { pluginApk1 { businessName = 'plugin-app' partKey = 'plugin-app' buildTask = ':plugin-app:assembleRelease' apkPath = 'plugin-app/build/outputs/apk/release/plugin-app-release.apk' } } } } loaderApkProjectPath = 'plugin-loader' runtimeApkProjectPath = 'plugin-runtime' version = 1 compactVersion = [1, 2, 3] uuidNickName = "1.0" } }
执行:
./gradlew packageDebugPlugin
即可得到plugin-debug.zip生成zip路径:
build/plugin-debug.zip
将 manager.apk 和 plugin-debug.zip 放到指定位置(demo中是host的assets目录下)即可享受。
主要参考: