基于Proxy思想的Android插件框架

基于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实现也是不完整的,包含不少回调都没有处理。同一时候我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/116004.html原文链接:https://javaforall.net

(1)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • Java 动静分离_如何做前后端动静分离

    Java 动静分离_如何做前后端动静分离我们的ERP系统就是前后端完全分离,毫无关联。后端用的是改造的Laravel框架,将业务拆分、路由拆分,来分离后端复杂的权限验证,同时对外依旧是简单明确的RESTfulAPI。前端采用Vue.js+Bootstrap构建。补充说明题主在问这个问题之前,有必要对HTTP协议有一定的了解,这样你就不会在一些细枝末节无限纠结。因为本质上前后端的区别就在于一个是请求方、一个是响应…

    2022年6月1日
    35
  • CNN笔记:通俗理解卷积神经网络

    CNN笔记:通俗理解卷积神经网络通俗理解卷积神经网络(cs231n与5月dl班课程笔记)1前言2012年我在北京组织过8期machinelearning读书会,那时“机器学习”非常火,很多人都对其抱有巨大的热情。当我2013年再次来到北京时,有一个词似乎比“机器学习”更火,那就是“深度学习”。本博客内写过一些机器学习相关的文章,但上一…

    2022年6月30日
    25
  • leetcode-149. 直线上最多的点数(map+判重)[通俗易懂]

    leetcode-149. 直线上最多的点数(map+判重)[通俗易懂]给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。示例 1:输入: [[1,1],[2,2],[3,3]]输出: 3解释:^|| o| o| o +————->0 1 2 3 4示例 2:输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]输出: 4解释:^|| o| o o| o| o o+—–

    2022年8月11日
    3
  • java 环境配置(详细教程)「建议收藏」

    java 环境配置(详细教程)「建议收藏」文章目录前言一、jdk下载二、windows1、jdk安装2、环境变量的配置3、检测是否配置成功前言java环境配置,网上教程很多,那我为什么还要写?首先为了完善我的知识体系今后一些软件的安装教程也可能会用到想写一个更加详细的,因为这并不仅仅是写给IT行业的,其它行业可能也需要配置java环境提示:以下是本篇文章正文内容,下面案例可供参考一、jdk下载如果你电脑已经下载了jdk,那就恭喜你可以跳过这一步了jdk的下载路径:https://www.oracle.co

    2022年7月9日
    16
  • LOAM 原理及代码实现介绍[通俗易懂]

    LOAM 原理及代码实现介绍[通俗易懂]LOAM介绍paper:《LidarOdometryandMappinginReal-time》LOAM整体框架:将定位与建图分开运行,高频位姿估计+低频优化建图->实现实时,低计算量,低漂移。数据提取及处理:根据点的曲率c来将点划分为不同的类别(边/面特征或不是特征),在每一个sweep中,根据曲率对点进行排序,来作为评价局部表面平滑性的标准。一个sweep指完成一次完整的扫描,一次sweep分为多个scan,每一次sweep的雷达位姿定义为为这一sweep起始时的状态

    2022年9月11日
    3
  • 【Oracle】RAC添加新节点

    【Oracle】RAC添加新节点

    2022年1月27日
    54

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号