Android插件化加载调研

[TOC]

插件化介绍

  • 插件化: 在主程序能独立运行的前提下,插件程序给主程序提供一些辅助的功能,目前主要以apk或者jar包的方式提供插件。
  • 动态加载:主程序在需要的时候才加载该功能的class文件,以达到减少内存占用的目的。
  • 主程序:支持插件功能的应用。大型app都有类似的需求。
  • 插件程序:给主程序作为补充的程序。例如一些二维码扫描之类的程序。

开源框架

  • dynamic-load-apk github地址
  • 360DroidPlugin github地址

实现原理介绍

如何加载插件里的class

DexClassloader

public DexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

具体参数的含义

Parameters:
    dexPath 需要装载的APK或者Jar文件的路径。包含多个路径用File.pathSeparator间隔开,在Android上默认是 ":" 
    optimizedDirectory  优化后的dex文件存放目录,不能为null
    libraryPath 目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为 null
    parent  该类装载器的父装载器,一般用当前执行类的装载器

classloader会有parent的classloader,这样查找类的时候不管是自己的还是parent的类都能够查找到。

具体查找过程

浅析dex文件加载机制

如何使用插件里的资源

在Android里,资源的获取是通过context里的两个方法来实现的

/** Return an AssetManager instance for your application's package. */

public abstract AssetManager getAssets();

/** Return a Resources instance for your application's package. */

public abstract Resources getResources();

重写这个两个方法,返回含有插件资源的Resource对象。

加载资源的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射, 代码如下:

try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }

只要想办法把这个重新实现的的context对象返回给插件,就可以加载插件的资源。这个后面会再讨论到。

如何启动插件中的activity

上面提到的两点基本上是业内已经确定的做法,后面则不是特别确定,我们分实现方式来讨论。

方式一:静态代理的模式 dynamic-load-apk

将Activity的生命周期方法提取出来作为一个接口(比如叫DLPlugin),然后通过代理Activity去调用插件Activity的生命周期方法,这样就完成了插件Activity的生命周期管理

public interface DLPlugin {
    public void onStart();
    public void onRestart();
    public void onActivityResult(int requestCode, int resultCode, Intent
    data);
    public void onResume();
    public void onPause();
    public void onStop();
    public void onDestroy();
    public void onCreate(Bundle savedInstanceState);
    public void setProxy(Activity proxyActivity, String dexPath);
    public void onSaveInstanceState(Bundle outState);
    public void onNewIntent(Intent intent);
    public void onRestoreInstanceState(Bundle savedInstanceState);
    public boolean onTouchEvent(MotionEvent event);
    public boolean onKeyUp(int keyCode, KeyEvent event);
    public void onWindowAttributesChanged(LayoutParams params);
    public void onWindowFocusChanged(boolean hasFocus);
    public void onBackPressed();
}

首先 插件中的Activity要继承DLBasePluginActivity

public class DLBasePluginActivity extends FragmentActivity implements DLPlugin {

然后主程序的Manifest文件中要注册用来代理的Activity


            
                

                
            
        

DLProxyActivity内容基本如下:

@Override
protected void onStart() {
     mRemoteActivity.onStart();
     super.onStart();
}
@Override
public AssetManager getAssets() {
     return impl.getAssets() == null ? super.getAssets() : impl.getAssets();
}

@Override
public Resources getResources() {
    return impl.getResources() == null ? super.getResources() : impl.getResources();
}

mRemoteActivity就是插件中的activity

这种方式的优势是实现简单易懂,缺点是插件必须基于插件库开发,必须要继承DLBasePluginActivity。

缺点:

  1. 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
  2. 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
  3. activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
  4. 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。

方式二: 反射的方式

@Override
protected void onResume() {
    super.onResume();
    Method onResume = mActivityLifecircleMethods.get("onResume");
    if (onResume != null) {
        try {
            onResume.invoke(mRemoteActivity, new Object[] { });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这种方式的优点是插件的apk不需要依赖任何的库文件,通过查找插件的AndroidManifest.xml文件中的activity,把activity启动起来,直接调用即可。缺点就是反射效率较低。

方式三 : Hook的方式

作者将原生的ActivityManager替换成自己的IActivityManagerHook。当startActivity时如果是插件的Activity,这个方法就会被Hook住,改成调用自己实现的startActivity方法。

IActivityManagerHook.java

@Override
public void onInstall(ClassLoader classLoader) throws Throwable {
Class cls = ActivityManagerNativeCompat.Class();
Object obj = FieldUtils.readStaticField(cls, "gDefault");
if (obj == null) {
   ActivityManagerNativeCompat.getDefault();
   obj = FieldUtils.readStaticField(cls, "gDefault");
}

if (IActivityManagerCompat.isIActivityManager(obj)) {
   setOldObj(obj);
   Class objClass = mOldObj.getClass();
   List> interfaces = Utils.getAllInterfaces(objClass);
   Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
   Object proxiedActivityManager = MyProxy.newProxyInstance(objClass.getClassLoader(), ifs, this);
   FieldUtils.writeStaticField(cls, "gDefault", proxiedActivityManager);
}

这样调用方法时,就会进入到invoke方法里,这里作者重写了所有的方法

public class IActivityManagerHookHandle extends BaseHookHandle {

    private static final String TAG = IActivityManagerHookHandle.class.getSimpleName();

    public IActivityManagerHookHandle(Context hostContext) {
        super(hostContext);
    }

    @Override
    protected void init() {
        sHookedMethodHandlers.put("startActivity", new startActivity(mHostContext));
        sHookedMethodHandlers.put("startActivityAsUser", new startActivityAsUser(mHostContext));
        sHookedMethodHandlers.put("startActivityAsCaller", new startActivityAsCaller(mHostContext));
        sHookedMethodHandlers.put("startActivityAndWait", new startActivityAndWait(mHostContext));
        sHookedMethodHandlers.put("startActivityWithConfig", new startActivityWithConfig(mHostContext));
        sHookedMethodHandlers.put("startActivityIntentSender", new startActivityIntentSender(mHostContext));
        sHookedMethodHandlers.put("startVoiceActivity", new 
        .////// more
    }

作者也是蛮拼的。

这样在activitymanager里面替换掉要启动的activityinfo和classloader,就可以启动activity了。
框架作者就是用这种方式,在application初始化的过程中将十几个Hook挂在各个系统API上。并且针对不同版本的API做了很多兼容工作,在用户调用系统API的时候框架悄无声息的运作着,使得框架非常易用,几乎任何已有的apk都可以作为插件运行起来。
个人觉得,DroidPlugin应当是目前最好的开源框架。

其余四大组件

其余和activity的做法类似,

  1. service一般需要预先注册,如果没有的话只能在主程序中注册一个serivce,用来管理所有插件的service。360的做法是注册了10个serivce,如果插件的serivce超过的10个话就把第一个启动的serivce替换掉。
  2. broadcast的话动态注册完全没有问题,静态注册的话要读取插件的注册信息,再动态注册一下就可以了,
  3. contentprovider试验了下一般可以直接访问。

如果使用插件中的view和fragment

这里我们用fragment来做示例。
分为两种情况:

  1. 插件的fragment是attach在插件的activity上,因为之前assetmanager已经通过代理模式重写过,所以没有关系。
  2. 插件的fragment是attach在主程序的activity上,这时会默认去读取主程序的资源,显然无法获取。
    但是fragment的getResource()方法是final的。无法重写。导致出现问题。

研究办法

  1. 重写support V4包,把final关键字去掉。
    但是因为不知道资源是属于主程序还是插件的,此方法仍然不能彻底解决问题。
  2. 修改aapt,给每个子apk中的资源分配不同头字节PackageID,这样就不会再互相冲突。

在Android中,所有资源会在Java源码层面生成对应的常量ID,这些ID会记录到R.java文件中,参与到之后的代码编译阶段中。在R.java文件中,Android资源在编译过程中会生成所有资源的ID,作为常量统一存放在R类中供其他代码引用。在R类中生成的每一个int型四字节资源ID,实际上都由三个字段组成。第一字节代表了Package,第二字节为分类,三四字节为类内ID。例如:

//android.jar中的资源,其PackageID为0x01
public static final int cancel = 0x01040000;

//用户app中的资源,PackageID总是0x7F
public static final int zip_code = 0x7f090f2e;

我们修改aapt后,是可以给每个子apk中的资源分配不同头字节PackageID,这样就不会再互相冲突。

修改完之后增加如下一行代码即可

 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            // 加入插件资源
            addAssetPath.invoke(assetManager, dexPath);
            //加入宿主资源
            addAssetPath.invoke(assetManager,
                    mContext.getApplicationInfo().sourceDir);

这是我们的做法,把所有资源加载一起,其实这时已经可以根据资源ID的前缀区分这个资源属于插件还是主程序,这样通过重写getResource动态来返回也是可以的

Android打包过程如下:

  1. 使用aapt生成R.java类文件:aapt.exe package -f -m -J
  2. 使用android SDK提供的aidl.exe把.aidl转成.java文件:aidl OPTIONS INPUT [OUTPUT]
  3. 编译.java类文件生成class文件: javac
  4. 使用android SDK提供的dx.bat命令行脚本生成classes.dex文件:dx.bat --dex --output=
  5. 使用Android SDK提供的aapt.exe生成资源包文件(包括res、assets、androidmanifest.xml等): aapt package
  6. 生成未签名的apk安装文件:apkbuilder
  7. 使用jdk的jarsigner对未签名的包进行apk签名: use jarsigner jarsigner -keystore

我们主要修改的就是第1步R文件的生成过程,然后第5步aapt会用R文件的值作为KEY,来查找真正的资源。

ps:其实根据我aapt dump的结果,图片等资源只是存储一个相对路径,例如drawable/*png,并没有带上包名,按道理说后面也是找不到的,但是demo的结果是可以找到的,后面会深入研究。
具体AAPT的修改方式如下:

aapt/Bundle.h
// 参数类,用来封装aapt的参数例如 aapt package -f
class Bundle {
public:
    Bundle(void)
        : mCmd(kCommandUnknown), mVerbose(false), mAndroidList(false),
          mForce(false), mGrayscaleTolerance(0), mMakePackageDirs(false),
          mUpdate(false), mExtending(false),
          mRequireLocalization(false), mPseudolocalize(false),
          mWantUTF16(false), mValues(false),
          mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL),
          mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL),
          mAutoAddOverlay(false), mGenDependencies(false),
          mAssetSourceDir(NULL), 
          mCrunchedOutputDir(NULL), mProguardFile(NULL),
          mAndroidManifestFile(NULL), mPublicOutputFile(NULL),
          mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL),
          mMinSdkVersion(NULL), mTargetSdkVersion(NULL), mMaxSdkVersion(NULL),
          mVersionCode(NULL), mVersionName(NULL), mCustomPackage(NULL), mExtraPackages(NULL),
          mMaxResVersion(NULL), mDebugMode(false), mNonConstantId(false), mProduct(NULL),
          mUseCrunchCache(false), mErrorOnFailedInsert(false), mOutputTextSymbols(NULL),
          mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(NULL),
          mArgc(0), mArgv(NULL), mIsPlugin(false)
        {}
    ~Bundle(void) {}
    
src/aapt/Resource.cpp

static status_t parsePackage(Bundle* bundle, const sp& assets,
    const sp& grp)
{
    // do something...
     assets->setPackage(String8(block.getAttributeStringValue(nameIndex, &len)));

    printf("======chenchen test ===package-verifier--> %s\n", assets->getPackage().string());
        //add by plugin
    if(indexOf(const_cast(assets->getPackage().string()), PLUGIN_PREFIX) != -1){
        bundle->setIsPlugin(true);
        printf("======chenchen test === it is plugin!!!!");
    }
}

在resource.cpp里,在解析完package的信息后,如果发现是插件,就将bundle里的参数设成true。

aapt/ResourceTable.cpp

ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage)
    : mAssetsPackage(assetsPackage), mNextPackageId(1), mHaveAppPackage(false),
      mIsAppPackage(!bundle->getExtending()),
      mNumLocal(0),
      mBundle(bundle)
{
    if(bundle->IsPlugin()){
        mPackageID = PLUGIN_PACKAGE_ID; //赋值
    }else {
        mPackageID = 127;
    }
    printf("==== package id is %d ====", mPackageID);
    
}

修改后的编译方法如下:

lunch sdk_eng
make aapt
# 如果想编译自己操作系统对应的aapt,直接编译就好,同时linux系统可以为window编译aapt可执行程序
window版本执行程序编译命令
USE_MINGW=1 OUT_DIR=out-x86 LOCAL_MULTILIB=32 make aapt

修改之后还有一个问题,就是主程序和插件必须使用不同的packageID,之前的想法是修改gradle打包的命令,最后采取修改在插件中定义meta-data,约定一个特殊的字符串,aapt打包时读取这个字符串,发现则认为是插件。没有的话还是正常的打包策略。这个meta-data的value就是给插件指定的id,这样就可以通过读取meta-data,识别这个资源id属于哪个插件。

assets文件夹的访问

插件和主程序的assets文件夹內的资源不可以有同名文件,应该约定加上一些前缀。如plugin_.mp3, host_.mp3

插件化优缺点讨论

优势

  1. 模块解耦,
  2. 动态升级,
  3. 高效并行开发(编译速度更快)
  4. 按需加载,内存占用更低
  5. 节省升级流量
  6. 65536限制
  7. 扩展方便,随时加入新功能

劣势

  1. 黑科技,依赖谷歌源码,需要适配系统版本
  2. 插件化SDK的实现较为复杂,容易出现难解决的bug
  3. 有些功能很难支持,例如自定义通知栏,activity的四种启动模式

你可能感兴趣的:(Android插件化加载调研)