插件化原理(以DL框架说明)

1. 插件化原理

DL框架的原理很简单:

在宿主apk中,有一个ProxyActivity,即代理Activity,这个Activity相当于一个空壳,插件中的Activity依靠ProxyActivity来对生命周期回调、资源加载以及启动另一个Activity等等。总而言之,ProxyActivity提供Context,插件Activity依靠ProxyActivity来做自己想做的事情。

2. DL的实现

首先看看ProxyActivity的实现:

public class ProxyActivity extends Activity {   

    private static final String TAG = "ProxyActivity";   

    public static final String FROM = "extra.from";   
    public static final int FROM_EXTERNAL = 0;   
    public static final int FROM_INTERNAL = 1;   

    public static final String EXTRA_DEX_PATH = "extra.dex.path";   
    public static final String EXTRA_CLASS = "extra.class";   

    private String mClass;   
    private String mDexPath;   

    @Override   
    protected void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
        mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);   
        mClass = getIntent().getStringExtra(EXTRA_CLASS);   

        Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);   
        if (mClass == null) {   
            launchTargetActivity();   
        } else {   
            launchTargetActivity(mClass);   
        }   
    }   

    @SuppressLint("NewApi")   
    protected void launchTargetActivity() {   
        PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(   
                mDexPath, 1);   
        if ((packageInfo.activities != null)   
                && (packageInfo.activities.length > 0)) {   
            String activityName = packageInfo.activities[0].name;   
            mClass = activityName;   
            launchTargetActivity(mClass);   
        }   
    }   

    @SuppressLint("NewApi")   
    protected void launchTargetActivity(final String className) {   
        Log.d(TAG, "start launchTargetActivity, className=" + className);   
        File dexOutputDir = this.getDir("dex", 0);   
        final String dexOutputPath = dexOutputDir.getAbsolutePath();   
        ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();   
        DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,   
                dexOutputPath, null, localClassLoader);   
        try {   
            Class<?> localClass = dexClassLoader.loadClass(className);   
            Constructor<?> localConstructor = localClass   
                    .getConstructor(new Class[] {});   
            Object instance = localConstructor.newInstance(new Object[] {});   
            Log.d(TAG, "instance = " + instance);   

            Method setProxy = localClass.getMethod("setProxy",   
                    new Class[] { Activity.class });   
            setProxy.setAccessible(true);   
            setProxy.invoke(instance, new Object[] { this });   

            Method onCreate = localClass.getDeclaredMethod("onCreate",   
                    new Class[] { Bundle.class });   
            onCreate.setAccessible(true);   
            Bundle bundle = new Bundle();   
            bundle.putInt(FROM, FROM_EXTERNAL);   
            onCreate.invoke(instance, new Object[] { bundle });   
        } catch (Exception e) {   
            e.printStackTrace();   
        }   
    }   

}

可以看到,这里的ProxyActivity目前只是实现从插件apk中加载类并实例化,然后再将这个ProxyActivity实例对象通过setProxy函数赋值给插件apk中的类。再调用apk中类的onCreate函数。
也就是说,插件apk中的Activity需要持有ProxyActivity的引用,即插件apk的Activity有了Context对象的引用,以后插件中的Activity启动第二个Activity或者是调用其他需要Context参数的函数时候,就可以通过ProxyActivity来做到了。
因为每个插件Activity都需要提供setProxy()函数,以及还需要判断:

当前的插件apk是用户自己安装的还是通过宿主apk加载出来的

所以,插件apk中所有的Activity需要一系列的相同代码,我们只需创建一个BaseActivity,把这些工作放到BaseActivity,这样就可以避免写重复代码,而且也可以让我们插件中的Activity更“像”正常的Activity。

看看BaseActivity:

import android.app.Activity;   
import android.content.Intent;   
import android.os.Bundle;   
import android.util.Log;   
import android.view.View;   
import android.view.ViewGroup.LayoutParams;   

public class BaseActivity extends Activity {   

    private static final String TAG = "Client-BaseActivity";   

    public static final String FROM = "extra.from";   
    public static final int FROM_EXTERNAL = 0;   
    public static final int FROM_INTERNAL = 1;   
    public static final String EXTRA_DEX_PATH = "extra.dex.path";   
    public static final String EXTRA_CLASS = "extra.class";   

    public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";   
    public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";   

    protected Activity mProxyActivity;   
    protected int mFrom = FROM_INTERNAL;   

    public void setProxy(Activity proxyActivity) {   
        Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);   
        mProxyActivity = proxyActivity;   
    }   

    @Override   
    protected void onCreate(Bundle savedInstanceState) {   
        if (savedInstanceState != null) {   
            mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);   
        }   
        if (mFrom == FROM_INTERNAL) {   
            super.onCreate(savedInstanceState);   
            mProxyActivity = this;   
        }   
        Log.d(TAG, "onCreate: from= " + mFrom);   
    }   

    protected void startActivityByProxy(String className) {   
        if (mProxyActivity == this) {   
            Intent intent = new Intent();   
            intent.setClassName(this, className);   
            this.startActivity(intent);   
        } else {   
            Intent intent = new Intent(PROXY_VIEW_ACTION);   
            intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);   
            intent.putExtra(EXTRA_CLASS, className);   
            mProxyActivity.startActivity(intent);   
        }   
    }   

    @Override   
    public void setContentView(View view) {   
        if (mProxyActivity == this) {   
            super.setContentView(view);   
        } else {   
            mProxyActivity.setContentView(view);   
        }   
    }   

    @Override   
    public void setContentView(View view, LayoutParams params) {   
        if (mProxyActivity == this) {   
            super.setContentView(view, params);   
        } else {   
            mProxyActivity.setContentView(view, params);   
        }   
    }   

    @Deprecated   
    @Override   
    public void setContentView(int layoutResID) {   
        if (mProxyActivity == this) {   
            super.setContentView(layoutResID);   
        } else {   
            mProxyActivity.setContentView(layoutResID);   
        }   
    }   

    @Override   
    public void addContentView(View view, LayoutParams params) {   
        if (mProxyActivity == this) {   
            super.addContentView(view, params);   
        } else {   
            mProxyActivity.addContentView(view, params);   
        }   
    }   
}

可以看到,在BaseActivity的onCreate函数中,通过判断当前的Activity启动时来自宿主Apk还是用户自己安装来设置mProxyActivity对象,如果是用户自己安装的,直接赋值this,如果是来自宿主apk的调用,则赋值为宿主中的ProxyActivity对象。

3. DL的资源管理和生命周期

前面我们的ProxyActivity只是简单回调了onCreate函数,还有其他的函数根本没有调用。另外,插件APK中的Activity的布局文件是插件APK中的资源文件,并不存在于宿主的APK中,无法通过ProxyActivity的Context来加载,因此,如果插件中的Activity使用类似R.layout.XXX的方式会出错。接下来要解决两个问题。

3.1 资源管理

既然插件中的Activity无法直接通过ProxyActivity这个Context来加载,那么我们可以通过反射机制,修改ProxyActivity中跟加载资源相关的api,使得能直接通过ProxyActivity加载资源。
Context中,跟资源相关的api就是如下两个抽象函数:

 /** 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();

也就是说,只要我们重写这两个函数,使得返回的AssetManager和Resources对象在加载资源时,能够去我们的插件apk中寻找资源,这样就可以解决问题啦。
首先,我们看看如何构造能够从插件apk中查找资源的AssetManager和Resources对象。通过loadResources函数来构造这两个对象,代码如下所示:

protected void loadResources() {   
    try {   

        AssetManager assetManager = AssetManager.class.newInstance();   
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);   
         //通过反射机制,调用AssetManager对象的addAssetPath函数 
         //传入的String参数mDexPath就是我们的插件apk路径 
         //为assetManager对象添加一条资源查找目录 
        addAssetPath.invoke(assetManager, mDexPath);   
        //将我们构造出来的AssetManager对象保存到成员变量 
        mAssetManager = assetManager;   
    } catch (Exception e) {   
        e.printStackTrace();   
    }   
    Resources superRes = super.getResources();   
    //构造Resources对象 
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),   
            superRes.getConfiguration());   
    mTheme = mResources.newTheme();   
    mTheme.setTo(super.getTheme());   
}

说明:加载的方法是通过反射.

通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中.

由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。

/**  
 * Add an additional set of assets to the asset manager.  This can be  
 * either a directory or ZIP file.  Not for use by applications.  Returns  
 * the cookie of the added asset, or 0 on failure.  
 * {@hide}  
 */   
public final int addAssetPath(String path) {   
    int res = addAssetPathNative(path);   
    return res;   
}

接下来就是,我们重写那两个抽象函数:

    @Override   
    public AssetManager getAssets() {   
        return mAssetManager == null ? super.getAssets() : mAssetManager;   
    }   

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

好啦,资源加载的问题解决了,我们可以在插件中的Activity使用R开头的资源啦!

3.2 生命周期

个人感觉,生命周期比较好管理,因为ProxyActivity本身就是有生命周期的,只需在ProxyActivity中回调插件Activty的对应的生命周期函数即可。
首先,把需要回调的生命周期函数通过反射的方法,保存起来:

//传入的localClass即为插件中的Activty 
protected void instantiateLifecircleMethods(Class<?> localClass) {   
    String[] methodNames = new String[] {   
            "onRestart",   
            "onStart",   
            "onResume",   
            "onPause",   
            "onStop",   
            "onDestory"   
    };   
    for (String methodName : methodNames) {   
        Method method = null;   
        try {   
            method = localClass.getDeclaredMethod(methodName, new Class[] { });   
            method.setAccessible(true);   
        } catch (NoSuchMethodException e) {   
            e.printStackTrace();   
        }   
        mActivityLifecircleMethods.put(methodName, method);   
    }   

    Method onCreate = null;   
    try {   
        onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });   
        onCreate.setAccessible(true);   
    } catch (NoSuchMethodException e) {   
        e.printStackTrace();   
    }   
    mActivityLifecircleMethods.put("onCreate", onCreate);   

    Method onActivityResult = null;   
    try {   
        onActivityResult = localClass.getDeclaredMethod("onActivityResult",   
                new Class[] { int.class, int.class, Intent.class });   
        onActivityResult.setAccessible(true);   
    } catch (NoSuchMethodException e) {   
        e.printStackTrace();   
    }   
    mActivityLifecircleMethods.put("onActivityResult", onActivityResult);   
}

然后,在ProxyActivity的各个生命周期函数中,对插件Activity的生命周期函数进行回调:

@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();   
        }   
    }   
}   

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

现在就解决了生命周期的问题啦!

4. 总结

总结一下DL框架的思想:
其本质就是,整个过程中,在Android系统中管理的Activity只是宿主apk中的ProxyActivity,而插件中的Activity本质就是个普通类而已,只不过ProxyActivity所有的逻辑代码(资源加载、布局显示等等)通过插件中的Activity来完成。
因此,在理解的时候,可以参照如下:

站在宿主apk的角度去想的时候:

插件中的Activity只是ProxyActivity调用的“工具类”,这个“工具类”帮助ProxyActivity执行布局显示等等相关的逻辑。

站在插件Activity的角度去想的时候:

当前插件Activity没有实际的上下文,我们需要借助ProxyActivity提供的上下文来完成Android系统环境中的api的调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值