Android的动态加载插件

Android的动态加载插件apk
分析
动态加载主要分为加载使用插件的资源和管理插件的Activity、service、BroadcastReceiver的功能
1.插件的资源加载
我们都知道要获Res下的文件,需要用Resource对象,但是apk是未安装的,宿主并没有对应的resId,因此获取资源需要进行反编译,反编译需要对应的插件的包名,就是反编译R资源。 贴代码,举个例子: 插件管理器类
/**
* Description:插件管理器
* @author chenby
* @create 2019/6/13 14: 34
public class PluginManager {
private static final String DEX_CACHE_PATH = "dex_cache";
* dex的缓存目录
private String dexCachePath;
private final HashMap<String, PluginPackage> pluginMap = new HashMap<>();
private PluginManager() {
* 静态内部类
private static class SingleTonHolder {
private static final PluginManager INSTANCE = new PluginManager();
* 单例模式
public static final PluginManager getInstance() {
return PluginManager.SingleTonHolder.INSTANCE;
private void initDexCacheDirectory(Context context) {
File dexFile = context.getDir(DEX_CACHE_PATH, Context.MODE_PRIVATE);
if (!dexFile.exists()) {
dexFile.mkdir();
dexCachePath = dexFile.getAbsolutePath();
* 加载某个目录下的插件
public void loadPlugins(Context context, String pluginFolder) {
initDexCacheDirectory(context);
File file = new File(pluginFolder);
File[] plugins = file.listFiles();
if (plugins == null || plugins.length == 0) {
return;
for (File plugin : plugins) {
if (plugin != null) {
loadPlugin(context, plugin.getAbsolutePath());
* 加载对应的某个插件
* @param context
* @param pluginPath
public void loadPlugin(Context context, String pluginPath) {
PackageInfo dexPackageInfo = getPackageInfo(context, pluginPath);
if (dexPackageInfo != null) {
//获取插件apk的包名
String dexPackageName = dexPackageInfo.packageName;
//获取插件apk对应的AssertManager对象
AssetManager dexAssertManager = getAssetManager(pluginPath);
//获取插件apk的资源对象
Resources dexResource = getResource(context, dexAssertManager);
//获取插件apk的类加载器
DexClassLoader dexClassLoader = getDexClassLoader(context, pluginPath);
PluginPackage pluginPackage = new PluginPackage(dexPackageName, dexClassLoader,
dexAssertManager, dexResource, dexPackageInfo);
pluginMap.put(dexPackageName, pluginPackage);
* 获取插件包
* @param packageName 对应的插件包名
* @return
public PluginPackage getPluginPackage(String packageName) {
return pluginMap.get(packageName);
* 获取插件apk对应的包信息
private PackageInfo getPackageInfo(Context context, String pluginPath) {
//取得PackageManager引用
PackageManager manager = context.getPackageManager();
//通过apk包文件路径获取到这个包的信息, (检索在包归档文件中定义的应用程序包的总体信息)
PackageInfo dexPackageArchiveInfo = manager.getPackageArchiveInfo(pluginPath,
PackageManager.GET_ACTIVITIES);
return dexPackageArchiveInfo;
* 获取插件apk对应的AssetManager对象
* @param pluginPath 插件的路径
private AssetManager getAssetManager(String pluginPath) {
try {
// 创建AssetManager实例 通过反射获取AssetManager 用来加载外面的资源包
AssetManager assetManager = AssetManager.class.newInstance();
Class cls = AssetManager.class;
Method method = cls.getMethod("addAssetPath", String.class);
// 反射设置资源加载路径
method.invoke(assetManager, pluginPath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
return null;
* 构造出插件apk对应的Resource对象
* @param assetManager {@link #getAssetManager(String)}
private Resources getResource(Context context, AssetManager assetManager) {
Resources resources = new Resources(assetManager,
context.getResources().getDisplayMetrics(),
context.getResources().getConfiguration());
return resources;
* 构造出插件apk对应的DexClassLoader对象
* @param pluginPath 插件的路径
private DexClassLoader getDexClassLoader(Context context, String pluginPath) {
DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, dexCachePath, null,
context.getClassLoader());
return dexClassLoader;
}
插件包信息的实体类
public class PluginPackage {
* 插件的包名
private String dexPackageName;
* 插件的Dex的类加载器
private DexClassLoader dexClassLoader;
* 插件的AssetManager对象
private AssetManager dexAssetManager;
* 插件的资源对象
private Resources dexResource;
* 插件的包信息
private PackageInfo dexPackageInfo;
public PluginPackage(String dexPackageName, DexClassLoader dexClassLoader,
AssetManager dexAssetManager, Resources dexResource,
PackageInfo dexPackageInfo) {
this.dexPackageName = dexPackageName;
this.dexClassLoader = dexClassLoader;
this.dexAssetManager = dexAssetManager;
this.dexResource = dexResource;
this.dexPackageInfo = dexPackageInfo;
public String getDexPackageName() {
return dexPackageName;
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
public AssetManager getDexAssetManager() {
return dexAssetManager;
public Resources getDexResource() {
return dexResource;
public PackageInfo getDexPackageInfo() {
return dexPackageInfo;
}
资源管理器类
这边的资源文件采用的是className = packageName + ".R$" + type 反编译资源类
package com.jason.dyload.manager;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import java.io.File;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
* Description:资源加载管理器(用来加载外部apk包的资源管理器)
* @author chenby
* @create 2019/5/30 15: 45
public class ResourceLoadManager {
private static final String RESOURCE_TYPE_DRAWABLE = "drawable";// 图片
private static final String RESOURCE_TYPE_STRING = "string";// 文字
private static final String RESOURCE_TYPE_COLOR = "color";// 颜色
private Context context;
private Resources dexResources;
private String dexPackageName;
private DexClassLoader dexClassLoader;
private ResourceLoadManager() {
* 静态内部类
private static class SingleTonHolder {
private static final ResourceLoadManager INSTANCE = new ResourceLoadManager();
* 单例模式
* @return
public static final ResourceLoadManager getInstance() {
return SingleTonHolder.INSTANCE;
public void init(Context context, PluginPackage pluginPackage) {
this.context = context.getApplicationContext();
dexPackageName = pluginPackage.getDexPackageName();
dexClassLoader = pluginPackage.getDexClassLoader();
dexResources = pluginPackage.getDexResource();
public Resources getDexResources() {
return dexResources;
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
public String getDexPackageName() {
return dexPackageName;
* 获取颜色
* @param resId 资源id
public int getColor(int resId) {
if (dexResources == null) {
return ContextCompat.getColor(context, resId);
String resName = dexResources.getResourceEntryName(resId);
int outResId = dexResources.getIdentifier(resName, RESOURCE_TYPE_COLOR, dexPackageName);
if (outResId == 0) {
return ContextCompat.getColor(context, resId);
return dexResources.getColor(outResId);
* 获取drawable资源
* @param resName 资源名称
public int getColor(String resName) {
int outResId = getResourceID(dexPackageName, RESOURCE_TYPE_COLOR, resName);
return dexResources.getColor(outResId);
* 获取drawable资源
* @param resId 资源id
public Drawable getDrawable(int resId) {//获取图片
if (dexResources == null) {
return ContextCompat.getDrawable(context, resId);
String resName = dexResources.getResourceEntryName(resId);
int outResId = dexResources.getIdentifier(resName, RESOURCE_TYPE_DRAWABLE, dexPackageName);
if (outResId == 0) {
return ContextCompat.getDrawable(context, resId);
return dexResources.getDrawable(outResId);
* 获取drawable资源
* @param resName 资源名称
public Drawable getDrawable(String resName) {
int outResId = getResourceID(dexPackageName, RESOURCE_TYPE_DRAWABLE, resName);
return dexResources.getDrawable(outResId);
* 获取未安装资源String
* @param resId 资源id
public String getString(int resId) {
if (dexResources == null) {
return context.getString(resId);
String resName = dexResources.getResourceEntryName(resId);
int outResId = dexResources.getIdentifier(resName, RESOURCE_TYPE_STRING, dexPackageName);
if (outResId == 0) {
return context.getString(resId);
return dexResources.getString(outResId);
* 获取drawable资源
* @param resName 资源名称
public String getString(String resName) {
int outResId = getResourceID(dexPackageName, RESOURCE_TYPE_STRING, resName);
return dexResources.getString(outResId);
* 获取未安装资源的ID
* @param packageName 包名
* @param type 资源类型
* @param fieldName 资源名
* @return
public int getResourceID(String packageName, String type, String fieldName) {
int resID = 0;
String rClassName = packageName + ".R$" + type;
try {
Class cls = dexClassLoader.loadClass(rClassName);
resID = (Integer) cls.getField(fieldName).get(null);
} catch (Exception e) {
e.printStackTrace();
return resID;
}
2.插件的Activity管理,这边只做了native页面的管理
定义插件和宿主共同的接口,放在单独的module,让宿主和插件的module同时引用
import android.app.Activity;
import android.os.Bundle;
* Description:PluginInterface
* @author chenby
* @create 2019/5/31 11: 18
public interface PluginInterface {
void onPluginCreate(Bundle saveInstance);
void attachContext(Activity context);
void onPluginStart();
void onPluginResume();
void onPluginRestart();
void onPluginDestroy();
void onPluginStop();
void onPluginPause();
}
宿主的实现
代理Activity
用来管理插件的Activity的生命周期
, 说白了就是把一个有生命周期的空activity,套上一个没有生命周期的activity上,通过反编译的形式获取到插件activity的类对象
package com.jason.dyload;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import com.jason.dyload.manager.ResourceLoadManager;
import com.jason.pluginlib.PluginInterface;
* Description:ProxyActivity
* @author chenby
* @create 2019/5/31 11: 22
public class ProxyActivity extends Activity {
private PluginInterface pluginInterface;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//拿到要启动的Activity
String className = getIntent().getStringExtra("className");
//String packageName = getIntent().getStringExtra("packageName");
try {
//加载该Activity的字节码对象
Class<?> aClass = ResourceLoadManager.getInstance().getDexClassLoader().loadClass(className);
//创建该Activity的示例
Object newInstance = aClass.newInstance();
//程序健壮性检查
if (newInstance instanceof PluginInterface) {
pluginInterface = (PluginInterface) newInstance;
//将代理Activity的实例传递给三方Activity
pluginInterface.attachContext(this);
//创建bundle用来与三方apk传输数据
Bundle bundle = new Bundle();
//调用三方Activity的onCreate,
pluginInterface.onPluginCreate(bundle);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
* 注意:三方调用拿到对应加载的三方Resources
@Override
public Resources getResources() {
return ResourceLoadManager.getInstance().getDexResources();
@Override
public void startActivity(Intent intent) {
String targetClassName = intent.getComponent().getClassName();
String packageName = ResourceLoadManager.getInstance().getDexPackageName();
intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", targetClassName);
intent.putExtra("packageName", packageName);
super.startActivity(intent);
@Override
public void onStart() {
if(pluginInterface != null) {
pluginInterface.onPluginStart();
super.onStart();
@Override
public void onResume() {
if(pluginInterface != null) {
pluginInterface.onPluginResume();
super.onResume();
@Override
public void onRestart() {
if(pluginInterface != null) {
pluginInterface.onPluginRestart();
super.onRestart();
@Override
public void onDestroy() {
if(pluginInterface != null) {
pluginInterface.onPluginDestroy();
super.onDestroy();
@Override
public void onStop() {
if(pluginInterface != null) {
pluginInterface.onPluginStop();
super.onStop();
@Override
public void onPause() {
if(pluginInterface != null) {
pluginInterface.onPluginPause();
super.onPause();
}
插件的实现
基类
public class BaseActivity extends Activity implements PluginInterface {
protected Activity thisContext;
@Override
public void setContentView(int layoutResID) {
if(thisContext != null) {
thisContext.setContentView(layoutResID);
}else {
setContentView(layoutResID);
@Override
public void setContentView(View view) {
if(thisContext != null) {
thisContext.setContentView(view);
}else {
setContentView(view);
@Override
public LayoutInflater getLayoutInflater() {
if(thisContext != null) {
return thisContext.getLayoutInflater();
}else {
return super.getLayoutInflater();
@Override
public Window getWindow() {
if(thisContext != null) {
return thisContext.getWindow();
}else {
return super.getWindow();
@Override
public View findViewById(int id) {
if(thisContext != null) {
return thisContext.findViewById(id);
}else {
return super.findViewById(id);
@Override
public ClassLoader getClassLoader() {
if(thisContext != null) {
return thisContext.getClassLoader();
}else {
return super.getClassLoader();
@Override
public WindowManager getWindowManager() {
return thisContext.getWindowManager();
@Override
public ApplicationInfo getApplicationInfo() {
return thisContext.getApplicationInfo();
@Override
public void finish() {
thisContext.finish();
public void onBackPressed() {
thisContext.onBackPressed();
@Override
public void startActivity(Intent intent) {
thisContext.startActivity(intent);
@Override
public void onPluginCreate(Bundle saveInstance) {
@Override
public void attachContext(Activity context) {
thisContext = context;
@Override
public void onPluginStart() {
@Override
public void onPluginResume() {
@Override
public void onPluginRestart() {
@Override
public void onPluginDestroy() {
@Override
public void onPluginStop() {
@Override
public void onPluginPause() {
}
实现类
public class PluginMainActivity extends BaseActivity implements View.OnClickListener {
@Override
public void onPluginCreate(Bundle saveInstance) {
super.onPluginCreate(saveInstance);
setContentView(R.layout.activity_plugin_main);
findViewById(R.id.btn).setOnClickListener(this);
@Override
public void onClick(View v) {
Intent intent = new Intent(thisContext, SecondActivity.class);
intent.putExtra("packageName", "com.jason.plugin");
startActivity(intent);
}
插件的第二个页面
public class SecondActivity extends BaseActivity {
@Override
public void onPluginCreate(Bundle saveInstance) {
super.onPluginCreate(saveInstance);
Toast.makeText(thisContext, "SecondActivity show", Toast.LENGTH_SHORT).show();
setContentView(R.layout.activity_second);
Log.i("chenby","thisContext == null: "+(thisContext == null));
}
宿主调用activity的页面和引用资源
package com.jason.dyload;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.LoaderManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.jason.dyload.manager.PluginManager;
import com.jason.dyload.manager.ResourceLoadManager;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private static int REQ_PERMISSION_CODE = 1001;
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_EXTERNAL_STORAGE };
private TextView showTv;
private ImageView showIv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
showTv = findViewById(R.id.tv_show);
showIv = findViewById(R.id.iv_show);
checkAndRequestPermissions();
* 权限检测以及申请
private void checkAndRequestPermissions() {
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
//File apkFile = new File(Environment.getExternalStorageDirectory(), "plugin.apk");
//PluginManager.getInstance().loadPlugin(this, apkFile.getAbsolutePath());
String pluginFolder = Environment.getExternalStorageDirectory() + "/dyLoad";
PluginManager.getInstance().loadPlugins(this, pluginFolder);
//初始化插件资源
ResourceLoadManager.getInstance()
.init(this, PluginManager.getInstance().getPluginPackage("com.jason.plugin"));
} else {
ActivityCompat.requestPermissions(this, PERMISSIONS, REQ_PERMISSION_CODE);
* 权限判断
private boolean hasPermission(String permissionName) {
return ActivityCompat.checkSelfPermission(this, permissionName)
== PackageManager.PERMISSION_GRANTED;
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQ_PERMISSION_CODE) {
checkAndRequestPermissions();
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
public void onLoadInternal(View view) {
showIv.setImageResource(R.drawable.girl1);
showTv.setText(getResources().getString(R.string.test_demo));
showTv.setTextColor(getResources().getColor(R.color.yellow));
showTv.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg));
public void onLoadExternal(View view) {
ResourceLoadManager.getInstance()
.init(this, PluginManager.getInstance().getPluginPackage("com.jason.plugin"));
showIv.setImageDrawable(ResourceLoadManager.getInstance().getDrawable("girl"));
showTv.setText(ResourceLoadManager.getInstance().getString("test_one"));
showTv.setTextColor(ResourceLoadManager.getInstance().getColor("black"));
showTv.setBackgroundDrawable(ResourceLoadManager.getInstance().getDrawable("bg"));
public void onLoadExternalPage(View view) {
ResourceLoadManager.getInstance()
.init(this, PluginManager.getInstance().getPluginPackage("com.jason.plugin"));
Intent intent = new Intent(this, ProxyActivity.class);
String packageName = "com.jason.plugin";
String otherApkMainActivityName = PluginManager.getInstance()
.getPluginPackage(packageName)
.getDexPackageInfo().activities[0].name;
intent.putExtra("className", otherApkMainActivityName);
intent.putExtra("packageName", packageName);
startActivity(intent);
public void onLoadExternal2Page(View view) {
ResourceLoadManager.getInstance()
.init(this, PluginManager.getInstance().getPluginPackage("com.jason.plugin2"));
Intent intent = new Intent(this, ProxyActivity2.class);
String packageName = "com.jason.plugin2";
String otherApkMainActivityName = PluginManager.getInstance()
.getPluginPackage(packageName)
.getDexPackageInfo().activities[0].name;
intent.putExtra("className", otherApkMainActivityName);