Android插件化入门

Android插件化入门

插件化是什么

用通俗易懂的话就是,它就像我们的U盘,可以机时的插在个电脑。不单单的U盘,还有显示器,显卡和CPU等电脑配件都是可以插入主板提供的接口,这些电脑组件通过主板提示的接口组合在一起就可以组成一部具有完整功能的电脑。

即然电脑可以以这种形式进行组装,哪我们android程序是不是也可以这样?答案是肯定的,我们的各个独立的功能模块都可以打包成apk,让宿主程序把apk加载进来,再运行里面的各个activity,service等

插件化的分类

插件化在技术难度上可以为分两种:独立插件化和非独立插件

  • 非独立插件是宿主程序与插件发开约定好插件开发规则,插件开发者要遵循这个规划进行开发,这种方式要求的技术难度相对来说低很多,但是增加了开发者的调用成本。类似比较成熟的解新局面方案有Small,需要说明的是small支持Android和iOS两个平台

  • 独立插件完全支持android的兼容四大组件的大部份的属性,这种方式对于开发者可以说是完全透明,是十分完善的插件化解决方案。但是偏写这类框架需要对android底层的代码非常熟悉,要对种种api进行hook处理,所以技术要求也非常的高。类似比较成熟的方案有DroidPlugin等等

插件化优缺点

  1. 优点

    • 模块间的解耦

    • 解除单个dex方法65535的限制

    • 动态更新,使我们的运营更加的灵活

  2. 缺点

    • 增加了程序开发的复杂度

    • 技术门槛更高

非独立插件原理

因为我们下载的apk里面activity没有在宿入的manifest.xml注册,如果我们直接调用startActivity方法,就会报activity没有注册的异常。我们可以在宿主activity中先注册一个代理Activity,然后通过宿主activity去调用插件里面的activity的方法。

哪我们怎样去加载我们插件apk里面的类呢?下面让我们了解下java里面几个类加载器:

  • DexClassLoader :可以加载文件系统上的jar、dex、apk

  • PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk

  • URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类

由上面的解释可以了解到我们可以通过DexClassLoader把插件apk里面的类加载出来让我们使用。

非独立插件实现

首先我们先摸拟apk下载的流程,把assets目录下面的apk下载下来,存放在缓存目录中,但是下载下来我们还不能直接使用,我们还要对apk包进行以下处理

解密我们下载下来的apk包

校验apk的签名是否正确

校验apk包所需的权限是否在主包中都包函


File mApkPath = this.getDir(ProxyActivity.APK_PATH, MODE_PRIVATE);

try {
    String mApkName = "myapplication-release-unsigned.apk";
    InputStream inputStream = getResources().getAssets().open(mApkName);

    byte[] datas = FileUtil.readFromInputStream(inputStream);

    String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
    FileUtil.writeByteToFile(datas, new File(fullPath));
} catch (IOException e) {
    e.printStackTrace();
}

然后我们就启动插件,但是由于非独立插件开发的时,宿主程序对插件模块是没有用引的,显示启动启件的方式显然是行不通的,所以只能通过隐式调用,具体调用如下。


Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra(ProxyActivity.APK_FILE_PATH, "myapplication-release-unsigned.apk");
intent.putExtra(ProxyActivity.ACTIVITY_NAME, "com.sundar.myapplication.LoginActivity");
startActivity(intent);

细心的读者或者己经发现,上面即然提到我们宿主程对插件是没用引用的,那为什么intent创建时我们会带上ProxyActivity的类呢?

其实我们是通过代理Activity来对插件apk进行加载,然后通过宿主ProxyActivity来实现插件的生命周期调用,下面给出实现代码


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    try {
        Intent intent = getIntent();
        mApkName = intent.getStringExtra(APK_FILE_PATH);
        mActivityName = intent.getStringExtra(ACTIVITY_NAME);
    } catch (Exception ignore) {
        // 
    }
    
    // 创建相关路径
    createFile();
    
    
    // 把当前的apk放到资源查找目录中
    mCustomAssetManager = new CustomAssetManager();
    mCustomAssetManager.addAssetPath(fullPath);
    
    // 创建classLoader加载类
    // dex解压释放后的目录
    File dexOutputDir = getDir(DEX_OP, 0);
    
    // apk存放的路径
    String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
    
    // 定义DexClassLoader
    // 第一个参数:是dex压缩文件的路径
    // 第二个参数:是dex解压缩后存放的目录
    // 第三个参数:是C/C++依赖的本地库文件目录,可以为null
    // 第四个参数:是上一级的类加载器
    mDexClassLoader = new DexClassLoader(fullPath, dexOutputDir.getAbsolutePath(), mSoPath.getAbsolutePath(), getClassLoader());
    
    // 通过代理的方法去生成Activity类
    try {
        Class<PluginActivity> pluginActivityClass = (Class<PluginActivity>) mDexClassLoader.loadClass(mActivityName);
        mPluginActivity = pluginActivityClass.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    // 加载类错误,需要显示错误信息给用户
    // 此类是插件activity
    if (mPluginActivity == null) {
        return;
    }
        
    //进行必要的初始化
    mPluginActivity.setmBaseActivity(this);
    // 实现对插件activity方法进行调用
    mPluginActivity.onCreate(savedInstanceState);
}


下面是创建插件Activity所需要的AssetsAssetManager用于加载插件资源的


public CustomAssetManager() {
    try {
        this.mAssetManager = AssetManager.class.newInstance();
        this.mAddedAssetsPath = new HashMap<>();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 添加apk的资源路径放到AssetManager里面
 */
public void addAssetPath(String apkPath) {
    if (mAssetManager == null) {
        return;
    }

    //先判断Map里面有没有己经添加的资源
    if (mAddedAssetsPath.containsKey(apkPath)) {
        return;
    }
    try {
        AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
                mAssetManager, apkPath);
        mAddedAssetsPath.put(apkPath, apkPath);
    } catch (Exception e) {
        e.printStackTrace();
    }
}



下面是我们插件Activity类的主要方法,大家或许会觉得好寄为什么要重写activity的方法呢?
答案是因为我们如果不重写直接设置就会报找不到相应的资源的异常。那我们是不是必须要重写这个方法呢?其实不是,我们可以把我们的插件apk添加到宿主apk的assetsManager里面,但是这里还会涉到到资源冲突的问题,具体解决资源冲突的方式可以自行百度


@Override
public void setContentView(@LayoutRes int layoutResID) {
    Resources resources = mBaseActivity.getmCustomAssetManager().getBundleResource(mBaseActivity);
    XmlResourceParser xmlResourceParser = resources.getLayout(layoutResID);
    View view  = LayoutInflater.from(mBaseActivity).inflate(xmlResourceParser, null);
    mBaseActivity.setContentView(view);
}

下面就是我们的效果图

6037478-09ae3fe1efed2f4d.jpg
472BD804-6650-49DB-9036-45D844A50881.png

非独立插件实现总结

以上就是非独立插件的实现过程,但是上面只是做了实现的原理,在做demo过程中,我遇到几个坑

  1. 因为android是通过AssetsManager去加载资源的,此时如果在配置文件中使用资源id去引用资源,系统则会抛出找不到资源的异常,而我们现在只能自己创建AssetsManager去获取apk包的资源

  2. 从上面的代码可以知道,插件的Activity的生成周期的调用只能通过代理Activity方法去调用

  3. 插件模块开发增加难度,插件开发者必须要尊守开发的规则

后续要做的事情

是否可以参考动态换肤的机制来实现对插件的资源进行加载,这样我们就可以直接使用配置文件里的资源,具体可以参考http://www.jianshu.com/p/af7c0585dd5b

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容