Understanding Android Plugin Architecture and a Minimal Plugin Implementation
This article explains the fundamentals of Android pluginization, covering class loading, resource handling, Dex and OAT formats, security considerations, and presents a lightweight plugin framework used by 58.com to reduce app size and enable dynamic updates.
The article begins by describing the pressure on mobile apps to reduce package size and the need for dynamic updates, introducing pluginization as a solution for modularizing functionality and resources.
It then details the Java class loading process, including the steps of loading, linking, and initialization, and explains how Android differs by using the Dex format and the ART runtime. The differences between class, Dex, odex, oat, and vdex files are clarified.
Two main loading strategies are compared: merged loading, which combines plugin Dex and native library paths into the host classloader, and independent loading, which uses a separate PluginDexClassLoader to avoid version conflicts. The article provides version‑specific reflection techniques for extending pathList.dexElements and nativeLibraryDirectories across Android versions.
Resource handling is examined, highlighting the challenges of resource ID collisions in merged mode and the advantages of independent mode. The PluginResourcesManager class is presented as a way to create isolated Resources and AssetManager instances for each plugin.
Four Android components (Activity, Service, Receiver, ContentProvider) are discussed, with three integration approaches: dynamic replacement via hooking, static proxy delegation, and pre‑embedding components in the host manifest. The article notes the early initialization of ContentProviders and suggests using placeholder providers.
A lightweight plugin framework used by 58.com is outlined. The build process generates plugin APKs and a plugin-upload-infos.json manifest. At runtime, plugins are managed under data/data/ /app_wbplugins , with versioned installation markers.
public final class PluginDexClassLoader extends BaseDexClassLoader {
private PluginDexClassLoader(List
dexPaths, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) throws Throwable {
super(dexPaths == null ? "" : TextUtils.join(File.pathSeparator, dexPaths),
optimizedDirectory, librarySearchPath, parent);
UnKnownFileTypeDexLoader.loadDex(this, dexPaths, optimizedDirectory);
}
static PluginDexClassLoader create(List
dexPaths, File optimizedDirectory,
File librarySearchFile) throws Throwable {
PluginDexClassLoader cl = new PluginDexClassLoader(dexPaths, optimizedDirectory,
librarySearchFile == null ? null : librarySearchFile.getAbsolutePath(),
PluginDexClassLoader.class.getClassLoader());
return cl;
}
} public final class PluginResourcesManager {
private static final Map
resourcesMap = new HashMap<>();
private static final Map
assetManagerMap = new HashMap<>();
public static Resources getResources(String pluginName) {
if (resourcesMap.containsKey(pluginName)) return resourcesMap.get(pluginName);
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = HiddenApiReflection.findMethod(AssetManager.class, "addAssetPath", String.class);
ApkPlugin apkPlugin = new ApkPlugin(pluginName, "", "");
File pluginApk = PluginPathManager.getInstance().getPluginApk(apkPlugin);
addAssetPath.invoke(assetManager, pluginApk.getAbsolutePath());
Resources pre = WBPluginLoader.getContext().getResources();
Resources res = new Resources(assetManager, pre.getDisplayMetrics(), pre.getConfiguration());
resourcesMap.put(pluginName, res);
assetManagerMap.put(pluginName, assetManager);
return res;
} catch (Throwable e) {
return WBPluginLoader.getContext().getResources();
}
}
public static AssetManager getAssetManager(String pluginName) {
if (assetManagerMap.containsKey(pluginName)) return assetManagerMap.get(pluginName);
return getResources(pluginName).getAssets();
}
} public final class PluginDelegateClassloader extends PathClassLoader {
private static BaseDexClassLoader originClassLoader;
PluginDelegateClassloader(ClassLoader parent) {
super("", parent);
originClassLoader = (BaseDexClassLoader) parent;
}
private static void reflectPackageInfoClassloader(Context baseContext, ClassLoader reflectClassLoader) throws Exception {
Object packageInfo = HiddenApiReflection.findField(baseContext, "mPackageInfo").get(baseContext);
if (packageInfo != null) {
HiddenApiReflection.findField(packageInfo, "mClassLoader").set(packageInfo, reflectClassLoader);
}
}
public static void inject(ClassLoader originalClassloader, Context baseContext) throws Exception {
Context ctx = baseContext;
while (ctx instanceof ContextWrapper) {
ctx = ((ContextWrapper) ctx).getBaseContext();
}
PluginDelegateClassloader classloader = new PluginDelegateClassloader(originalClassloader);
reflectPackageInfoClassloader(ctx, classloader);
}
@Override
protected Class
findClass(String name) throws ClassNotFoundException {
try {
return originClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
for (PluginDexClassLoader loader : PluginClassLoaders.getInstance().getClassLoaders()) {
Class
clazz = loader.loadClassItself(name);
if (clazz != null) return clazz;
}
throw e;
}
}
@Override
public String findLibrary(String name) {
String lib = originClassLoader.findLibrary(name);
if (lib == null) {
for (PluginDexClassLoader loader : PluginClassLoaders.getInstance().getClassLoaders()) {
lib = loader.findLibraryItself(name);
if (lib != null) break;
}
}
return lib;
}
}During implementation, issues such as missing signature files causing crashes in third‑party security SDKs and conflicts with AndroidX AppCompat resources were encountered and resolved by ensuring proper APK signing and avoiding AppCompat dependencies in plugins.
In conclusion, the article demonstrates that a well‑designed, lightweight plugin framework can effectively reduce app size, support dynamic updates, and maintain stability on Android platforms.
58 Tech
Official tech channel of 58, a platform for tech innovation, sharing, and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.