Android基于代理的插件化思路分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android基于代理的插件化思路分析相关的知识,希望对你有一定的参考价值。

前言

正常的App开发流程基本上是这样的:开发功能-->测试--->上线,上线后发现有大bug,紧急修复---->发新版本---->用户更新----->bug修复。从发现bug到修复bug花了很长时间。我们希望bug的修复是立马生效的,用户无感知就能自动修复bug。当然,android端的动态修复bug已经有不少框架了,不过我们今天讲的是另一个话题:Android的插件化。Android插件化有很多好处:热插拔、静默升级、bug动态修复、代码解耦等。正是因为如此,才有越来越多的公司选择插件化。

分析

Android插件化有很多的开源框架,基本上都是两种思路:代理和替换系统的一些关键变量,通过替换这些变量,达到欺骗系统的目的(又称Hook)。代理的思路比较简单,就是通过代理类Proxy,把主工程和插件工程的组件连接起来。代理类相当于傀儡,当主工程想要启动插件工程时,实际上会先调用代理类的相应方法,然后再通过代理类调用插件工程的组件,间接达到调用插件工程组件的目的。为何不直接调用插件工程的组件呢?因为通过DexClassLoader加载到内存的Activity等组件只是一个普通的类,没有上下文环境,意味着拿不到Context,意味着没有生命周期。

让Activity有"生命"

得益于Java语言的类加载器可以动态加载类的特性,在Android中加载一个普通的类是很容易的

DexClassLoader
 Class<?> mClassLaunchActivity = (Class<?>) 
classLoader.loadClass(mLaunchActivity);
        mPluginActivity = (IPluginActivity) 
mClassLaunchActivity.newInstance();

通过ClassLoader可以加载一个类,并生成类的实例。但是这个mPluginActivity只是一个普通的类,并没有Activity的生命周期,所以我们需要借助PluginProxyActivity来完成代理工作


public class PluginProxyActivity extends Activity {
    IPluginActivity mPluginActivity;
    String mPluginApkFilePath;
    String mLaunchActivity;
    private String mPluginName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle bundle = getIntent().getExtras();
        if(bundle == null){
            return;
        }
        mPluginName = bundle.getString(PluginUtils.PARAM_PLUGIN_NAME);
        mLaunchActivity = bundle.getString(PluginUtils.PARAM_LAUNCH_ACTIVITY);
        File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.this, mPluginName);
        if(!pluginFile.exists()){
            return;
        }
        mPluginApkFilePath = pluginFile.getAbsolutePath();
        try {
            initPlugin();
            mPluginActivity.IOnCreate(savedInstanceState);
        } catch (Exception e) {
            mPluginActivity = null;
            e.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if(mPluginActivity != null){
            mPluginActivity.IOnResume();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        if(mPluginActivity != null) {
            mPluginActivity.IOnStart();
        }
    }
    ........ //省略部分代码
    private void initPlugin() throws Exception {
        PackageInfo packageInfo;
        try {
            PackageManager pm = getPackageManager();
            packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES);
        } catch (Exception e) {
            throw e;
        }

        ClassLoader classLoader = PluginUtils.getOrCreateClassLoaderByPath(this, mPluginName, mPluginApkFilePath);
        // get default launchActivity if target Activity is null
        if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
            if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {
                throw new ClassNotFoundException("Launch Activity not found");
            }
            mLaunchActivity = packageInfo.activities[0].name;
        }
        Class<?> mClassLaunchActivity = (Class<?>) classLoader.loadClass(mLaunchActivity);

        mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();
        mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo);
    }


    protected Class<? extends PluginProxyActivity> getProxyActivity(String pluginActivityName) {
        return getClass();
    }

    @Override
    public void startActivityForResult(Intent intent, int requestCode) {
        boolean pluginActivity = intent.getBooleanExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
        if (pluginActivity) {
            String launchActivity = null;
            ComponentName componentName = intent.getComponent();
            if(null != componentName) {
                launchActivity = componentName.getClassName();
            }
            intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
            if (launchActivity != null && launchActivity.length() > 0) {
                Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
                pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
                pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
                pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
                startActivityForResult(pluginIntent, requestCode);
            }
        } else {
            super.startActivityForResult(intent, requestCode);
        }
    }
}

每次启动新的Activity的时候,都会调用startActivityForResult,在此方法中,进行了Intent的替换,启动的新Activity还是会跳到ProxyActivity中。

 Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
                pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
                pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
                pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
                startActivityForResult(pluginIntent, requestCode);

所有的插件工程都需要继承于BasePluginActivity,其主要代码如下


public class BasePluginActivity extends Activity implements IPluginActivity {

    private boolean mIsRunInPlugin;
    private ClassLoader mDexClassLoader;
    private Activity mOutActivity;
    private String mApkFilePath;
    private PackageInfo mPackageInfo;
    private PluginContext mContext;
    private View mContentView;
    private Activity mActivity;
    private boolean mFinished;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (mIsRunInPlugin) {
            mActivity = mOutActivity;
        } else {
            super.onCreate(savedInstanceState);
            mActivity = this;
        }
    }

    @Override
    public void setContentView(int layoutResID) {
        if (mIsRunInPlugin) {
            mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null);
            mActivity.setContentView(mContentView);
        } else {
            super.setContentView(layoutResID);
        }
    }

    @Override
    public void setContentView(View view) {
        if (mIsRunInPlugin) {
            mContentView = view;
            mActivity.setContentView(mContentView);
        } else {
            super.setContentView(view);
        }
    }

    @Override
    public View findViewById(int id) {
        if (mIsRunInPlugin && mContentView != null) {
            View v = mContentView.findViewById(id);
            if (null == v) {
                v = super.findViewById(id);
            }
            return v;
        } else {
            return super.findViewById(id);
        }
    }

    @Override
    public void IOnCreate(Bundle savedInstanceState) {
        onCreate(savedInstanceState);
    }

    @Override
    public void IOnResume() {
        onResume();
    }

    @Override
    public void IOnStart() {
        onStart();
    }

    @Override
    public void IOnPause() {
        onPause();
    }

    @Override
    public void IOnStop() {
        onStop();
    }

    @Override
    public void IOnDestroy() {
        onDestroy();
    }

    @Override
    public void IOnRestart() {
        onRestart();
    }

    @Override
    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
        mIsRunInPlugin = true;
        mDexClassLoader = classLoader;
        mOutActivity = context;
        mApkFilePath = path;
        mPackageInfo = packageInfo;

        mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
        attachBaseContext(mContext);
    }

    @Override
    protected void onResume() {
        if (mIsRunInPlugin) {
            return;
        }
        super.onResume();
    }

    @Override
    protected void onPause() {
        if (mIsRunInPlugin) {
            return;
        }
        super.onPause();

    }

    @Override
    protected void onStart() {
        if (mIsRunInPlugin) {
            return;
        }
        super.onStart();
    }

    @Override
    protected void onRestart() {
        if (mIsRunInPlugin) {
            return;
        }
        super.onRestart();
    }

    @Override
    protected void onStop() {
        if (mIsRunInPlugin) {
            return;
        }
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        if (mIsRunInPlugin) {
            mDexClassLoader = null;
            return;
        }
        super.onDestroy();
    }

    @Override
    public void finish() {
        if (mIsRunInPlugin) {
            int resultCode = Activity.RESULT_CANCELED;
            Intent data = null;
            synchronized (this) {
                Field field;
                try {
                    field = Activity.class.getDeclaredField("mResultCode");
                    field.setAccessible(true);
                    resultCode = (Integer) field.get(this);
                    field = Activity.class.getDeclaredField("mResultData");
                    field.setAccessible(true);
                    data = (Intent) field.get(this);
                } catch (Exception e) {
                }
            }
            mOutActivity.setResult(resultCode, data);
            mOutActivity.finish();
            mFinished = true;
        } else {
            super.finish();
        }
    }

    @Override
    public boolean isFinishing() {
        if (mIsRunInPlugin) {
            return mFinished;
        } else {
            return super.isFinishing();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (mIsRunInPlugin) {
            return;
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    @Override
    public LayoutInflater getLayoutInflater() {
        if (mContext != null) {
            return LayoutInflater.from(mContext);
        } else {
            return LayoutInflater.from(mActivity);
        }
    }

    @Override
    public WindowManager getWindowManager() {
        if (mIsRunInPlugin) {
            return mOutActivity.getWindowManager();
        } else {
            return super.getWindowManager();
        }
    }

    @Override
    public void startActivityForResult(Intent intent, int requestCode) {
        if (mIsRunInPlugin) {
            intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, true);
            mActivity.startActivityForResult(intent, requestCode);
        } else {
            super.startActivityForResult(intent, requestCode);
        }
    }
}

在BasePluginActivity中,覆写了很多父类Activity的方法,用来判断当前Activity是独立运行还是作为插件运行,如果是在插件中运行,则是调用插件中设置进来的代理类ProxyActivity(mActivity)的相应方法。同时使用IPluginActivity来模拟Activity的生命周期

public interface IPluginActivity {
    public void IOnCreate(Bundle savedInstanceState);

    public void IOnResume();

    public void IOnStart();

    public void IOnPause();

    public void IOnStop();

    public void IOnDestroy();

    public void IOnRestart();

    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
}

至此Activity已经有生命周期了!但是还有个问题,插件的资源如何获取?

插件资源的获取

这里就不卖关子了。直接通过反射调用AssetManager的addAssetPath方法把资源加载到Resource对象中,即可获取插件中的资源。为了后续方便,我们直接继承ContextWrapper类自己实现getAssets和getResources方法即可。代码如下

class PluginContext extends ContextWrapper {

    private AssetManager mAsset;
    private Resources mResources;
    private Theme mTheme;
    private int mThemeResId;
    private ClassLoader mClassLoader;
    private Context mOutContext;

    private AssetManager getSelfAssets(String apkPath) {
        AssetManager instance = null;
        try {
            instance = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(instance, apkPath);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return instance;
    }

    private Resources getSelfRes(Context ctx, AssetManager selfAsset) {
        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
        Configuration con = ctx.getResources().getConfiguration();
        return new Resources(selfAsset, metrics, con);
    }

    private Theme getSelfTheme(Resources selfResources) {
        Theme theme = selfResources.newTheme();
        mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");
        theme.applyStyle(mThemeResId, true);
        return theme;
    }
    
    

    private int getInnerRIdValue(String rStrnig) {
        int value = -1;
        try {
            int rindex = rStrnig.indexOf(".R.");
            String Rpath = rStrnig.substring(0, rindex + 2);
            int fieldIndex = rStrnig.lastIndexOf(".");
            String fieldName = rStrnig.substring(fieldIndex + 1, rStrnig.length());
            rStrnig = rStrnig.substring(0, fieldIndex);
            String type = rStrnig.substring(rStrnig.lastIndexOf(".") + 1, rStrnig.length());
            String className = Rpath + "$" + type;

            Class<?> cls = Class.forName(className);
            value = cls.getDeclaredField(fieldName).getInt(null);

        } catch (Throwable e) {
            e.printStackTrace();
        }
        return value;
    }


    public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
        super(base, themeres);
        mClassLoader = classLoader;
        mAsset = getSelfAssets(apkPath);
        mResources = getSelfRes(base, mAsset);
        mTheme = getSelfTheme(mResources);
        mOutContext = base;
    }

    @Override
    public Resources getResources() {
        return mResources;
    }

    @Override
    public AssetManager getAssets() {
        return mAsset;
    }

    @Override
    public Theme getTheme() {
        return mTheme;
    }
}

至此,插件化的两大难题已经解决。现在插件中的Activity可以启动了!用同样的思路可以完成其他组件的代码编写。

源码下载

PluginDemo

延伸阅读

资源加载和activity生命周期管理
基于Proxy思想的Android插件框架


以上是关于Android基于代理的插件化思路分析的主要内容,如果未能解决你的问题,请参考以下文章

Android 插件化Hook 插件化框架 ( Hook 技术 | 代理模式 | 静态代理 | 动态代理 )

Android 插件化“ 插桩式 “ 插件化框架 ( 代理 Activity 组件开发 )

Android 插件化Hook 插件化框架 ( Hook Activity 启动过程 | 静态代理 )

Android 插件化Hook 插件化框架 ( Hook Activity 启动流程 | AMS 启动前使用动态代理替换掉插件 Activity 类 )

Android 插件化“ 插桩式 “ 插件化框架 ( 原理与实现思路 )

Android插件化框架