Q1:Android 插件化的核心痛点是什么?原生系统为什么不支持直接运行插件APK?
答案:
- 四大组件(Activity/Service等)必须在宿主AndroidManifest.xml注册,系统才会识别,插件APK的组件未注册,直接启动会崩溃;
- 插件APK的类、资源无法被宿主加载,类加载器、资源路径不互通;
- 系统服务(AMS/WMS)校验组件合法性,拦截未注册组件。
核心解决思路 :占坑+Hook → 用宿主注册的占位组件骗过系统,再替换为插件组件执行。
流程图:
精简源码(占坑启动):
java
// 宿主预注册的占坑Activity
public class StubActivity extends Activity {}
public void startPluginActivity(Context context, Class<?> pluginClass) {
Intent intent = new Intent();
intent.putExtra("plugin_class", pluginClass.getName());
intent.setClass(context, StubActivity.class);
context.startActivity(intent);
}
Q2:插件化实现的两大核心技术方案是什么?区别是什么?
答案:
- 静态代理(占坑方案):宿主预注册占位组件,启动时替换,无系统源码侵入,兼容性好(Shadow采用)。
- 动态Hook方案:Hook AMS/PMS等系统服务,绕过校验,侵入性强,高版本易失效。
核心区别:是否侵入系统服务。
流程图:
精简源码:见Q1源码。
Q3:Shadow框架的核心定位是什么?相比传统插件化有什么优势?
答案 :
Shadow是腾讯自研的零Hook、纯安卓SDK接口实现 的插件化框架,定位免费、稳定、兼容所有Android版本、无Google Play合规风险 。
优势:无Hook、多插件/多进程、热修复、合规。
流程图:
精简源码(初始化):
java
Shadow.get().init(this);
Shadow.get().loadPlugin("plugin.apk", new LoadPluginCallback() {
@Override public void onSuccess() {
Shadow.get().startActivity(context, "com.plugin.MainActivity");
}
});
Q4:插件Activity如何实现生命周期正常分发?
答案:
- 宿主占坑Activity接管生命周期;
- 通过反射/接口回调,将占坑的生命周期分发给插件Activity;
- 插件Activity模拟执行。
流程图:
精简源码:
java
// 插件基类
public class PluginBaseActivity extends Activity {
public void pluginOnCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
// 占坑Activity
public class StubActivity extends Activity {
private PluginBaseActivity mPluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String className = getIntent().getStringExtra("plugin_class");
mPluginActivity = (PluginBaseActivity) Class.forName(className).newInstance();
mPluginActivity.attachContext(this);
mPluginActivity.pluginOnCreate(savedInstanceState);
}
}
Q5:插件Service/Receiver/ContentProvider 如何实现插件化?
答案:
- Service:宿主占坑Service,通过Binder转发生命周期。
- BroadcastReceiver:静态转动态,宿主注册后转发给插件。
- ContentProvider:宿主占坑Provider,Uri路由到插件Provider。
核心:统一占坑+路由转发。
流程图(Service):
精简源码(Service代理):
java
// 宿主占坑Service
public class StubService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String realService = intent.getStringExtra("real_service");
PluginService service = PluginClassLoader.loadClass(realService).newInstance();
service.onStartCommand(intent, flags, startId);
return START_STICKY;
}
}
Q6:插件Activity的Intent数据如何传递和还原?
答案 :
启动时将原始Intent存入extra,占坑中替换getIntent()返回值。
流程图:
精简源码:
java
// 启动时保存
intent.putExtra("original_intent", originalIntent);
// 占坑中还原
@Override
public Intent getIntent() {
Intent original = getIntent().getParcelableExtra("original_intent");
return original != null ? original : super.getIntent();
}
Q7:Android 类加载器双亲委托模型是什么?插件化如何打破它?
答案 :
双亲委托:子加载器优先委托父加载器。插件化需打破:插件类由独立ClassLoader优先自加载。
流程图:
精简源码:
java
public class PluginClassLoader extends DexClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
try { clazz = findClass(name); } // 插件优先
catch (ClassNotFoundException e) { clazz = super.loadClass(name, resolve); }
}
return clazz;
}
}
Q8:插件与宿主的类冲突如何解决?(如重复依赖Glide)
答案:
- 类隔离:独立ClassLoader;
- 依赖下沉:公共库只宿主依赖,插件provided;
- Shadow强隔离。
流程图:
精简源码(gradle配置):
groovy
// 插件build.gradle
dependencies {
provided 'com.github.bumptech.glide:glide:4.12.0' // 不打包进插件
}
Q9:Shadow的类加载方案是什么?
答案 :
多ClassLoader隔离,每个插件独立;无Hook;支持宿主类透传。
示意图:
精简源码:Shadow内部实现类似Q7的PluginClassLoader,并维护插件白名单。
Q10:插件资源加载的核心问题是什么?如何解决?
答案 :
宿主Resources只认宿主资源路径。解决:创建合并资源的Resources。
流程图:
精简源码:
java
public Resources createPluginResources(Context hostContext, String pluginApkPath) {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginApkPath);
Resources hostRes = hostContext.getResources();
return new Resources(assetManager, hostRes.getDisplayMetrics(), hostRes.getConfiguration());
}
Q11:资源ID冲突如何解决?插件化如何规避?
答案 :
资源ID = 0xPPTTEEEE,宿主和插件可能重复。
解决方案:AAPT2修改插件资源Package ID(如0x7F改成0x60),或Shadow自动隔离。
流程图:
精简源码:在插件build.gradle中配置:
groovy
android {
aaptOptions {
additionalParameters "--package-id", "0x60"
}
}
Q12:插件化中Context如何替换?为什么要替换?
答案 :
替换为插件自定义Context,用于绑定插件ClassLoader、Resources,拦截系统API。
流程图:
精简源码:
java
public class ShadowContext extends ContextWrapper {
private ClassLoader pluginClassLoader;
private Resources pluginResources;
@Override
public ClassLoader getClassLoader() { return pluginClassLoader; }
@Override
public Resources getResources() { return pluginResources; }
}
Q13:Shadow框架的核心组成部分有哪些?
答案 :
宿主Core、插件Runtime、占坑组件、Loader。
流程图:
精简源码(宿主初始化):见Q3源码。
Q14:Shadow如何实现插件的安装与加载?
答案 :
下载APK → 校验 → 创建目录 → 优化dex → 初始化ClassLoader/Resources → 启动占坑。
流程图:
精简源码:见Q3源码。
Q15:Shadow支持多插件、多进程吗?如何实现?
答案 :
支持。每个插件独立沙箱、独立ClassLoader;多进程通过Binder + PluginProcessService实现。
架构图:
Q16:Android 9.0+ 限制非SDK接口(Hook黑名单),对插件化有什么影响?
答案 :
传统Hook插件化完全失效;Shadow零Hook不受影响。
示意图:
Q17:插件化如何避免OOM、崩溃、稳定性问题?
答案 :
类隔离、生命周期同步、异常捕获沙箱、资源及时回收。
流程图:
Q18:插件化与热修复的区别是什么?Shadow是否支持热修复?
答案 :
插件化是组件级动态添加;热修复是代码级补丁。Shadow支持热修复(替换插件代码)。
对比图:
Q19:插件化的性能损耗在哪里?如何优化?
答案 :
损耗点:反射调用、首次类加载、资源合并。优化:反射缓存、预加载、公共库托管。
流程图:
精简源码(反射缓存):
java
private static Method sAddAssetPathMethod;
static {
sAddAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
}
Q20:插件化上线落地的核心风险点有哪些?如何规避?
答案 :
合规风险 → 用Shadow;版本兼容 → 依赖框架;崩溃传染 → 沙箱隔离;类/资源冲突 → 强隔离+依赖下沉。
流程图:
Q21:插件化中的 ClassLoader 泄露问题是如何产生的?如何避免?
答案 :
产生原因:插件卸载后仍有对象引用其ClassLoader加载的类。避免:WeakReference、生命周期清理、主动GC。
流程图:
精简源码:
java
public void unloadPlugin(String partKey) {
WeakReference<PluginClassLoader> ref = pluginLoaders.get(partKey);
if (ref != null) {
ref.clear();
pluginLoaders.remove(partKey);
}
System.gc();
}
Q22:如何实现插件化框架中自定义 Theme 的支持?
答案 :
创建插件独立Resources,插件Context的getTheme返回插件Theme。
流程图:
精简源码:
java
public class ShadowContext extends ContextWrapper {
private Resources pluginResources;
@Override
public Resources.Theme getTheme() {
Theme theme = pluginResources.newTheme();
theme.applyStyle(R.style.PluginTheme, true);
return theme;
}
}
Q23:简述 Android 插件化中 BroadcastReceiver 动态注册和静态注册的处理差异。
答案 :
动态注册天然支持;静态注册需解析Manifest并转为动态注册。
流程图:
精简源码:
java
for (ReceiverInfo receiver : pluginInfo.getReceivers()) {
IntentFilter filter = new IntentFilter(receiver.getAction());
BroadcastReceiver instance = (BroadcastReceiver) pluginClassLoader
.loadClass(receiver.getClassName()).newInstance();
hostContext.registerReceiver(instance, filter);
}
Q24:插件化框架如何实现插件之间、插件与宿主之间的通信?
答案 :
Intent+Binder、Callback、接口下沉+APT、EventBus。
流程图:见原Q24的mermaid图。
精简源码(Callback示例):
java
// 宿主定义接口
public interface HostCallback { void onEvent(String data); }
// 插件调用
ShadowApplication.get().getHostCallback().onEvent("hello");
Q25:插件化中如何加载插件的 so 库?需要注意哪些问题?
答案 :
通过DexClassLoader的libraryPath指定目录,提取so文件。注意架构匹配、路径权限、冲突隔离。
流程图:
精简源码:
java
public class PluginClassLoader extends DexClassLoader {
@Override
public String findLibrary(String name) {
String path = getPluginNativeDir() + "/" + System.mapLibraryName(name);
return new File(path).exists() ? path : super.findLibrary(name);
}
}
Q26:Shadow 的 Gradle 插件是如何将普通 App 代码转换成插件代码的?打包流程是怎样的?
答案 :
在Transform阶段用ASM将所有Activity父类替换为ShadowActivity,并打包为ZIP(plugin+loader+runtime)。
流程图:见原Q26的mermaid图。
Gradle配置:
groovy
shadow {
packagePlugin {
pluginInfo {
loaderApkName = "loader.apk"
runtimeApkName = "runtime.apk"
}
}
}
Q27:Shadow 框架中如何处理插件 Fragment 的生命周期?
答案 :
无需特殊处理,Fragment生命周期绑定于ShadowActivity,直接使用原生代码。
流程图:
示例代码:
java
// 插件Activity中直接使用
getSupportFragmentManager()
.beginTransaction()
.add(R.id.container, new MyPluginFragment())
.commit();
Q28:为什么启动插件 Activity 时不能直接使用 new Intent() 并 startActivity?必须执行 Intent 转换的根本原因是什么?
答案 :
AMS校验包名,插件包名未注册会导致崩溃。必须将Intent中的Component替换为宿主占坑组件。
流程图:见原Q28的mermaid时序图。
精简源码(Intent转换):
java
public Intent convertIntent(Intent pluginIntent) {
Intent stubIntent = new Intent(context, StubActivity.class);
stubIntent.putExtra("target_plugin_activity", pluginIntent.getComponent());
return stubIntent;
}
// 占坑Activity中还原
Intent realIntent = new Intent();
realIntent.setComponent((ComponentName) getIntent().getExtra("target_plugin_activity"));
setIntent(realIntent);
Q29:插件化的类加载隔离如何影响内存和性能?有哪些优化策略?
答案 :
通用库被多次加载,内存翻倍。优化:共享库白名单、懒加载、dex预编译、dex插桩复用。
流程图:
精简源码(dex插桩合并):
java
Object[] hostElements = getDexElements(hostClassLoader);
Object[] pluginElements = getDexElements(pluginLoader);
Object[] newElements = combine(pluginElements, hostElements);
setDexElements(hostClassLoader, newElements);