美文网首页
Android的动态加载插件

Android的动态加载插件

作者: JasonChen8888 | 来源:发表于2019-10-12 14:29 被阅读0次

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);
    intent.putExtra("packageName", packageName);
    startActivity(intent);
  }
}

插件获取的是插件的

总结一下:
1、加载插件资源:利用反射获取插件的AssertManager对象,然后利用获取到的AssertManager对象创建对应的插件包的Resource资源对象。接着创建插件包的DexClassLoader对象,利用DexClassLoader加载资源的R类,通过反射拿到插件的资源resId,再利用插件的Resource对象获取对应的资源id的资源。
2、管理插件的native窗口,插件和宿主同时定义共同的接口,宿主利用DexClassLoader反射加载插件的native窗口类,判断插件类是否继承了相同的接口,反射接口对象,去管理插件页面

相关文章

网友评论

      本文标题:Android的动态加载插件

      本文链接:https://www.haomeiwen.com/subject/iejpfctx.html