• 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插件框架

  • 相关阅读:
    Auto.js 入门教程
    几乎纯css实现弹出框
    php curl访问https 域名接口一直报错的问题
    金山打字通刷打字速度脚本
    centos 安装 图像识别工具 tesseract-ocr 流程
    js 实现俄罗斯方块(三)
    小程序报错 .wxss 无法找到
    Redis存储数组
    问题记录-databinding/hilt踩坑记录
    问题记录-CoordinatorLayout+WebView使用遇到的问题
  • 原文地址:https://www.cnblogs.com/jasonkent27/p/5883546.html
Copyright © 2020-2023  润新知