zoukankan      html  css  js  c++  java
  • 基于Proxy思想的Android插件框架

    意义

    研究插件框架的意义在于下面几点:

    • 减小安装包的体积,通过网络选择性地进行插件下发
    • 模块化升级。减小网络流量
    • 静默升级,用户无感知情况下进行升级
    • 解决低版本号机型方法数超限导致无法安装的问题
    • 代码解耦

    现状

    Android中关于插件框架的技术已经有过不少讨论和实现。插件通常打包成apk或者dex的形式。

    dex形式的插件往往提供了一些功能性的接口,这样的方式类似于java中的jar形式。仅仅是因为Android的Dalvik VM无法直接动态载入Java的Byte Code,所以须要我们提供Dalvik Byte Code。而dex就是Dalvik Byte Code形式的jar。

    apk形式的插件提供了比dex形式很多其它的功能,比如能够将资源打包进apk。也可实现插件内的Activity或者Service等系统组件。

    本文主要讨论apk形式的插件框架。对于apk形式又存在安装和不安装两种方式

    • 安装apk的方式实现相对简单。主要原理是通过将插件apk和主程序共享一个UserId,主程序通过createPackageContext构造插件的context,通过context就可以訪问插件apk中的资源,非常多app的主题框架就是通过安装插件apk的形式实现。比如Go主题。这样的方式的缺点就是须要用户手动安装,体验并非非常好。

    • 不安装apk的方式攻克了用户手动安装的缺点,但实现起来比較复杂,主要通过DexClassloader的方式实现。同一时候要解决怎样启动插件中Activity等Android系统组件。为了保证插件框架的灵活性,这些系统组件不太好在主程序中提前声明,实现插件框架真正的难点在此。

    DexClassloader

    这里引用《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版里对java类载入器的一段描写叙述:

    虚拟机设计团队把类载入阶段中的“通过一个类的全限定名来获取描写叙述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定怎样去获取所须要的类。实现这个动作的代码模块称为“类载入器”。

    Android虚拟机的实现參考了java的JVM。因此在Android中载入类也用到了类载入器的概念,仅仅是相对于JVM中载入器载入class文件而言。Android的Dalvik虚拟机载入的是Dex格式,而详细完毕Dex载入的主要是PathClassloaderDexclassloader

    PathClassloader默认会读取/data/dalvik-cache中缓存的dex文件,未安装的apk假设用PathClassloader来载入,那么在/data/dalvik-cache文件夹下找不到相应的dex。因此会抛出ClassNotFoundException

    DexClassloader能够载入随意路径下包括dex和apk文件,通过指定odex生成的路径,可载入未安装的apk文件。

    以下一段代码展示了DexClassloader的用法:

    final File optimizedDexOutputPath = context.getDir("odex", Context.MODE_PRIVATE);
    try{
        DexClassLoader classloader = new DexClassLoader("apkPath",
                optimizedDexOutputPath.getAbsolutePath(),
                null, context.getClassLoader());
        Class<?> clazz = classloader.loadClass("com.plugindemo.test");
        Object obj = clazz.newInstance();
        Class[] param = new Class[2];
        param[0] = Integer.TYPE;
        param[1] = Integer.TYPE;
        Method method = clazz.getMethod("add", param);
        method.invoke(obj, 1, 2);
    }catch(InvocationTargetException e){
        e.printStackTrace();
    }catch(NoSuchMethodException e){
        e.printStackTrace();
    }catch(IllegalAccessException e){
        e.printStackTrace();
    }catch(ClassNotFoundException e){
        e.printStackTrace();
    }catch (InstantiationException e){
        e.printStackTrace();
    }

    DexClassloader攻克了类的载入问题,假设插件apk里仅仅是一些简单的API调用。那么上面的代码已经能满足需求。只是这里讨论的插件框架还须要解决资源訪问和Android系统组件的调用。

    插件内系统组件的调用

    Android Framework中包括ActivityServiceContent Provider以及BroadcastReceiver等四大系统组件。这里主要讨论怎样在主程序中启动插件中的Activity。其他3种组件的调用方式类似。

    大家都知道Activity须要在AndroidManifest.xml中进行声明。apk在安装的时候PackageManagerService会解析apk中的AndroidManifest.xml文件,这时候就决定了程序包括的哪些Activity,启动未声明的Activity会报ActivityNotFound异常。相信大部分Android开发人员以前都遇到过这个异常。

    启动插件里的Activity必定会面对怎样在主程序中的AndroidManifest.xml中声明这个Activity,然而为了保证插件框架的灵活性。我们是无法预知插件中有哪些Activity,所以也无法提前声明。

    为了解决上述问题,这里介绍一种基于Proxy思想的解决方法,大致原理是在主程序的AndroidManifest.xml中声明一些ProxyActivity。启动插件中的Activity会转为启动主程序中的一个ProxyActivityProxyActivity中全部系统回调都会调用插件Activity中相应的实现,最后的效果就是启动的这个Activity实际上是主程序中已经声明的一个Activity,可是相关代码运行的却是插件Activity中的代码。这就攻克了插件Activity未声明情况下无法启动的问题,从上层来看启动的就是插件中的Activity。以下详细分析整个过程。

    PluginSDK

    全部的插件和主程序须要依赖PluginSDK进行开发,全部插件中的Activity继承自PluginSDK中的PluginBaseActivityPluginBaseActivity继承自Activity并实现了IActivity接口。

    public interface IActivity {
        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);
    }
    public class PluginBaseActivity extends Activity implements IActivity {
        ...
        private Activity mProxyActivity;
        ...
        
        @Override
        public void IInit(String path, Activity context, ClassLoader classLoader) {
            mProxy = true;
            mProxyActivity = context;
    
            mPluginContext = new PluginContext(context, 0, path, classLoader);
            attachBaseContext(mPluginContext);
        }
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            if (mProxy) {
                mRealActivity = mProxyActivity;
            } else {
                super.onCreate(savedInstanceState);
                mRealActivity = this;
            }
        }
    
        @Override
        public void setContentView(int layoutResID) {
            if (mProxy) {
                mContentView = LayoutInflater.from(mPluginContext).inflate(layoutResID, null);
                mRealActivity.setContentView(mContentView);
            } else {
                super.setContentView(layoutResID);
            }
        }
    
        ...
    
        @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();
        }
    }
    public class ProxyActivity extends Activity {
        IActivity mPluginActivity;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Bundle bundle = getIntent().getExtras();
            if(bundle == null){
                return;
            }
            mPluginName = bundle.getString(PluginConstants.PLUGIN_NAME);
            mLaunchActivity = bundle.getString(PluginConstants.LAUNCH_ACTIVITY);
            File pluginFile = PluginUtils.getInstallPath(ProxyActivity.this, mPluginName);
            if(!pluginFile.exists()){
                return;
            }
            mPluginApkFilePath = pluginFile.getAbsolutePath();
            try {
                initPlugin();
                super.onCreate(savedInstanceState);
                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 = PluginUtils.getPackgeInfo(this, mPluginApkFilePath);
    
            if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
                mLaunchActivity = packageInfo.activities[0].name;
            }
    
            ClassLoader classLoader = PluginUtils.getClassLoader(this, mPluginName, mPluginApkFilePath);
    
            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 = classLoader.loadClass(mLaunchActivity); getIntent().setExtrasClassLoader(classLoader); mPluginActivity = (IActivity) mClassLaunchActivity.newInstance(); mPluginActivity.IInit(mPluginApkFilePath, this, classLoader); } ... @Override public void startActivityForResult(Intent intent, int requestCode) { boolean pluginActivity = intent.getBooleanExtra(PluginConstants.IS_IN_PLUGIN, false); if (pluginActivity) { String launchActivity = null; ComponentName componentName = intent.getComponent(); if(null != componentName) { launchActivity = componentName.getClassName(); } intent.putExtra(PluginConstants.IS_IN_PLUGIN, false); if (launchActivity != null && launchActivity.length() > 0) { Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity)); pluginIntent.putExtra(PluginConstants.PLUGIN_NAME, mPluginName); pluginIntent.putExtra(PluginConstants.PLUGIN_PATH, mPluginApkFilePath); pluginIntent.putExtra(PluginConstants.LAUNCH_ACTIVITY, launchActivity); startActivityForResult(pluginIntent, requestCode); } } else { super.startActivityForResult(intent, requestCode); } }

    PluginBaseActivityProxyActivity在整个插件框架的核心,以下简单分析一下代码:

    首先看一下ProxyActivity#onResume

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

    变量mPluginActivity的类型是IActivity,因为插件Activity实现了IActivity接口,因此能够推測mPluginActivity.IOnResume()终于运行的是插件Activity的onResume中的代码,以下我们来证实这样的推測。

    PluginBaseActivity实现了IActivity接口,那么这些接口详细是怎么实现的呢?看代码:

    @Override
    public void IOnCreate(Bundle savedInstanceState) {
        onCreate(savedInstanceState);
    }
    
    @Override
    public void IOnResume() {
        onResume();
    }
    
    @Override
    public void IOnStart() {
        onStart();
    }
    
    @Override
    public void IOnPause() {
        onPause();
    }
    
    ...

    接口实现很easy,仅仅是调用了和接口相应的回调函数。那这里的回调函数终于会调到哪里呢?前面提到过全部插件Activity都会继承自PluginBaseActivity。也就是说这里的回调函数终于会调到插件Activity中相应的回调,比方IOnResume运行的是插件Activity中的onResume中的代码。这也证实了之前的推測。

    上面的一些代码片段揭示了插件框架的核心逻辑。其他的代码很多其他的是为实现这样的逻辑服务的。后面会提供整个project的源代码,大家可自行分析理解。

    插件内资源获取

    实现载入插件apk中的资源的一种思路是将插件apk的路径增加主程序资源查找的路径中。以下的代码展示了这样的方法:

    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;
    }

    为了让插件Activity訪问资源时使用我们自己定义的Context,我们须要在PluginBaseActivity的初始化中做一些处理:

    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
        mProxy = true;
        mProxyActivity = context;
    
        mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
        attachBaseContext(mContext);
    }

    PluginContext中通过重载getAssets来实现包括插件apk查找路径的Context:

    public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
        super(base, themeres);
        mClassLoader = classLoader;
        mAsset = getPluginAssets(pluginFilePath);
        mResources = getPluginResources(base, mAsset);
        mTheme = getPluginTheme(mResources);
    }
    
    private AssetManager getPluginAssets(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 getPluginAssets(Context ctx, AssetManager selfAsset)  {
        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
        Configuration con = ctx.getResources().getConfiguration();
        return new Resources(selfAsset, metrics, con);
    }
    
    private Theme getPluginTheme(Resources selfResources) {
        Theme theme = selfResources.newTheme();
        mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");
        theme.applyStyle(mThemeResId, true);
        return theme;
    }
    
    @Override
    public Resources getResources() {
        return mResources;
    }
    
    @Override
    public AssetManager getAssets() {
        return mAsset;
    }
    
    ...

    总结

    本文介绍了一种基于Proxy思想的插件框架,全部的代码都在Github中,代码仅仅是抽取了整个框架的核心部分,假设要用在生产环境中还须要完好,比方Content ProviderBroadcastReceiver组件的Proxy类未实现,Activity的Proxy实现也是不完整的,包含不少回调都没有处理。同一时候我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。

  • 相关阅读:
    Java实现 LeetCode 343 整数拆分(动态规划入门经典)
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 342 4的幂
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 341 扁平化嵌套列表迭代器
    Java实现 LeetCode 338 比特位计数
    H264(NAL简介与I帧判断)
    分享一段H264视频和AAC音频的RTP封包代码
  • 原文地址:https://www.cnblogs.com/yxwkf/p/5196235.html
Copyright © 2011-2022 走看看