首页 > 其他分享 >Android插件化(一)技术调研

Android插件化(一)技术调研

时间:2024-09-17 22:53:46浏览次数:11  
标签:patchfile 插件 APK env 组件 Android public 调研

Android插件化(一)技术调研

前言

有关APK更新的技术比较多,例如:增量更新、插件式开发、热修复、RN、静默安装。
下面简单介绍一下:

更新方式签名
增量更新旧版本Apk(v1.0)和新(v2.0)、旧版本Apk(v1.0)生成的差分包(apk.patch 质量小)合并成为新版本Apk(v2.0)安装。
插件式开发给宿主APK提供插件,扩展(需要的时候再下载),可以动态地替换。主要技术是动态代理的知识。
热修复通过NDK底层去修复,也是C/C++的技术。
RN通过JS脚本去修复APK。
静默安装需要root权限,适配不同手机ROM很麻烦。

插件化、热修复(思想)的发展历程

  • 2012年7月,AndroidDynamicLoader,大众点评,陶毅敏:思想是通过Fragment以及schema的方式实现的,这是一种可行的技术方案,但是还有限制太多,这意味这你的activity必须通过Fragment去实现,这在activity跳转和灵活性上有一定的不便,在实际的使用中会有一些很奇怪的bug不好解决,总之,这还是一种不是特别完备的动态加载技术。
  • 2013年,23Code,自定义控件的动态下载:主要利用 Java ClassLoader 的原理,可动态加载的内容包括 apk、dex、jar等。
  • 2014年初,Altas,阿里伯奎的技术分享:提出了插件化的思想以及一些思考的问题,相关资料比较少。
  • 2014年底,Dynamic-load-apk,任玉刚:动态加载APK,通过Activity代理的方式给插件Activity添加生命周期。
  • 2015年4月,OpenAltas/ACCD:Altas的开源项目,一款强大的Android非代理动态部署框架,目前已经处于稳定状态。
  • 2015年8月,DroidPlugin,360的张勇:DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制:通过Hook思想来实现,它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
  • 2015年9月,AndFix,阿里:通过NDK的Hook来实现热修复。
  • 2015年11月,Nuwa,大众点评:通过dex分包方案实现热修复。
  • 2015年底,Small,林光亮:打通了宿主与插件之间的资源与代码共享。
  • 2016年4月,ZeusPlugin,掌阅:ZeusPlugin最大特点是:简单易懂,核心类只有6个,类总数只有13个。

1.增量更新

增量更新就是原有app的基础上只更新发生变化的地方,其余保持原样。
与原来每次更新都要下载完整apk包的做法相比,这样做的好处显而易见:每次变化的地方总是比较少,因此更新包的体积就会小很多。

1.1增量更新的流程
  1. APP检测最新版本:把当前版本告诉服务端,服务端进行判断。
    如果有新版本,服务端需要对当前版本的APK与最新版本的APK进行一次差分,产生patch差分文件。(或者新版本的APK上传到服务端的时候就已经差分好了)
  2. APP在后台下载差分文件,进行文件的MD5校验,在本地进行合并(跟本地的data目录下面的APK文件合并),合并出最新的APK之后,提示用户安装。
  3. 增量更新的最终目的:省流量地更新宿主APK。

差分的处理比较麻烦的地方就是要针对不同的应用市场渠道和众多不同版本进行差分。
注意:新版本有可能比旧版本小,差分只是把变化的部分记录下来。

1.2服务器端行为(后台工程师操作)
1.2.1下载拆分和合并要用的第三方库(bsdiff、bzip2)

我们使用到的第三方库是:Binary diff,简称bsdiff,这个库专门用来实现文件的差分和合并的,它的官网如下:http://www.daemonology.net/bsdiff/

1.2.2Java代码调用:

创建Web项目,用来做APP的服务端。创建工具类专门用于产生差分包:

public class BsDiff {
    /**
     * 差分
     * @param oldfile
     * @param newfile
     * @param patchfile
     */
    public native static void diff(String oldfile,String newfile,String patchfile);

    static {
        System.loadLibrary("bsdiff");
    }
}

其中JNI的实现如下(该实现写在bsdiff.cpp中):

JNIEXPORT void JNICALL Java_com_haocai_bsdiff_BsDiff_diff
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr) {
    int argc = 4;
    char* oldfile = (char*)env->GetStringUTFChars(oldfile_jstr, NULL);
    char* newfile = (char*)env->GetStringUTFChars(newfile_jstr, NULL);
    char* patchfile = (char*)env->GetStringUTFChars(patchfile_jstr, NULL);

    //参数(第一个参数无效)
    char *argv[4];
    argv[0] = { "bsdiff" };
    argv[1] = oldfile;
    argv[2] = newfile;
    argv[3] = patchfile;

    bsdiff_main(argc, argv);

    env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
    env->ReleaseStringUTFChars(newfile_jstr, newfile);
    env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
};

通过研究bsdiff的源码,我们发现bsdiff.cpp里面的main函数就是入口函数,避免歧义把函数名main改为bsdiff_main,然后通过JNI去调用。根据bsdiff.cpp中bsdiff_main函数方法中有以下关键语句

if (argc != 4) errx(1, "usage: %s oldfile newfile patchfile\n", argv[0]);

根据提示需要传入4个参数:

    argv[0] = "bsdiff";//这个参数没用
    argv[1] = oldPath;//旧APK文件路径
    argv[2] = newPath;/新APK文件路径
    argv[3] = patchPath;//APK差分文件路径

然后我们准备两个APK文件,不同版本的,最好Java代码、资源都不一样。

写一个Java测试类生成差分包:

package com.haocai.bsdiff;

public class ConstantsWin {

    //路径不能包含中文
    public static final String OLD_APK_PATH = "D:/android_apks/test_old.apk";

    public static final String NEW_APK_PATH = "D:/android_apks/test_new.apk";

    public static final String PATCH_PATH = "D:/android_apks/apk.patch";
}
package com.haocai.bsdiff;

/**
 * Created by Administrator on 2017/11/14.
 */
public class BsDiffTest {
    public static void main(String[] args){
        //得到差分包
        BsDiff.diff(ConstantsWin.OLD_APK_PATH,ConstantsWin.NEW_APK_PATH,ConstantsWin.PATCH_PATH);
    }
}

注意:

  • test_new.apk、test_old.apk 要先放在目标目录
  • bsdiff.cpp中生成差分包的程序方法是异步的,所以生成完整的apk.patch可能要等一下。apk.patch体积大小停止增长,表示生成结束。
1.2.3简单搭建后台JavaWeb供Android前端下载apk.patch差分包
1.3Android客户端行为
1.3.1编译合并要用的第三方库(bsdiff、bzip2)

对应的Java代码如下:

package com.haocai.app.update;

/**
 * Created by Xionghu on 2017/11/14.
 * Desc:
 */

public class BsPatch {
    /**
     * 合并
     * @param oldfile
     * @param newfile
     * @param patchfile
     */
    public native static void patch(String oldfile,String newfile,String patchfile);

    static {
        System.loadLibrary("bspatch");
    }
}

在Android端,我们需要把bzip2以及bsdiff的文件拷贝到jni目录里面,同样的,我们只需要编译一个bspatch.c源文件即可。

//合并
JNIEXPORT void JNICALL Java_com_haocai_app_update_BsPatch_patch
  (JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){
    int argc = 4;
    char* oldfile = (char*)(*env)->GetStringUTFChars(env,oldfile_jstr, NULL);
    char* newfile = (char*)(*env)->GetStringUTFChars(env,newfile_jstr, NULL);
    char* patchfile = (char*)(*env)->GetStringUTFChars(env,patchfile_jstr, NULL);

    //参数(第一个参数无效)
    char *argv[4];
    argv[0] = "bspatch";
    argv[1] = oldfile;
    argv[2] = newfile;
    argv[3] = patchfile;

    bspatch_main(argc,argv);

    (*env)->ReleaseStringUTFChars(env,oldfile_jstr, oldfile);
    (*env)->ReleaseStringUTFChars(env,newfile_jstr, newfile);
    (*env)->ReleaseStringUTFChars(env,patchfile_jstr, patchfile);

  }

代码v1.0差分包合并核心代码如下:

package com.haocai.app.update;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.format.Formatter;
import android.widget.Toast;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.callback.FileCallback;
import com.lzy.okgo.model.Progress;
import com.lzy.okgo.model.Response;
import com.lzy.okgo.request.base.Request;
import java.io.File;
import java.text.NumberFormat;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_PERMISSION_STORAGE = 0x01;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 0:
                    Toast.makeText(MainActivity.this, "您正在进行省流量更新", Toast.LENGTH_SHORT).show();
                    ApkUtils.installApk(MainActivity.this, Constants.NEW_APK_PATH);
                    break;
            }
        }
    };
    private NumberFormat numberFormat;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setTitle("简单文件下载");

        numberFormat = NumberFormat.getPercentInstance();
        numberFormat.setMinimumFractionDigits(2);

        checkSDCardPermission();

        /**
         * 因为后台没有写版本判断语句
         * 在高版本下暂时先注释fileDownload(); 否则一直下载安装
         *
         * 低版本下运行fileDownload();
         */
         fileDownload();


    }


    /**
     * 检查SD卡权限
     */
    protected void checkSDCardPermission() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_STORAGE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSION_STORAGE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //获取权限
                fileDownload();
            } else {
                Toast.makeText(getApplicationContext(), "权限被禁止,无法下载文件!", Toast.LENGTH_SHORT).show();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //Activity销毁时,取消网络请求
        OkGo.getInstance().cancelTag(this);
    }


    public void fileDownload() {

        OkGo.<File>get(Constants.URL_PATCH_DOWNLOAD)//
                .tag(this)//
                .execute(new FileCallback(Constants.SD_CARD, Constants.PATCH_FILE) {

                    @Override
                    public void onStart(Request<File, ? extends Request> request) {
                    }

                    @Override
                    public void onSuccess(Response<File> response) {

                        new Thread(new Runnable() {
                            @Override
                            public void run() {

                                try {
                                    //      File patchFile = new File(Constants.SD_CARD, Constants.PATCH_FILE);
                                    String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
                                    String newfile = Constants.NEW_APK_PATH;
                                    String patchfile = Constants.SD_CARD + File.separator + Constants.PATCH_FILE;
                                    BsPatch.patch(oldfile, newfile, patchfile);

                                    mHandler.sendEmptyMessage(0);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }
                        }).start();


                    }

                    @Override
                    public void one rror(Response<File> response) {

                    }

                    @Override
                    public void downloadProgress(Progress progress) {
                        System.out.println(progress);

                        String downloadLength = Formatter.formatFileSize(getApplicationContext(), progress.currentSize);
                        String totalLength = Formatter.formatFileSize(getApplicationContext(), progress.totalSize);
                        String speed = Formatter.formatFileSize(getApplicationContext(), progress.speed);
                        System.out.println(downloadLength);
                    }
                });
    }

}

注意:这里7.0可能会有问题,把路径暴露给别的app,需要FileProvider去实现(不难,这个留给大家去做吧)。

2.插件化

插件化框架的一些对比,下面引用
https://github.com/wequick/Small/blob/master/Android/COMPARISION.md

特性DynamicLoadApkDynamicAPKSmallDroidPluginVirtualAPKRePlugin
支持四大组件只支持Activity只支持Activity只支持Activity全支持全支持全支持
组件无需在宿主manifest中预注册×
插件可以依赖宿主×
支持PendingIntent×××
Android特性支持大部分大部分大部分几乎全部几乎全部几乎全部
兼容性适配一般一般中等
插件构建部署aaptGradle插件Gradle插件Gradle插件
源码https://github.com/singwhatiwanna/dynamic-load-apkhttps://github.com/CtripMobile/DynamicAPKhttps://github.com/wequick/Smallhttps://github.com/DroidPluginTeam/DroidPluginhttps://github.com/didi/VirtualAPKhttps://github.com/Qihoo360/RePlugin
开发者singwhatiwannaCtripMobileLody滴滴360
2.1DynamicLoadApk

基于静态代理的实现

2.2VirtualAPK
2.2.1特性
FeatureDetail
Supported componentsActivity, Service, Receiver and Provider
Manually register components in AndroidManifest.xmlNo need
Access host app classes and resourcesSupported
PendingIntentSupported
Supported Android featuresAlmost all features
CompatibilityAlmost all devices
Building systemGradle plugin
Supported Android versionsAPI Level 15+
2.2.2架构

![[Android插件化(一)技术调研.png]]

2.2.3原理
2.2.3.1基本原理
  • 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复
  • 合并插件和宿主的资源 重设插件资源的packageId,将插件资源和宿主资源合并
  • 去除插件包对宿主的引用 构建时通过Gradle插件去除插件对宿主的代码以及资源的引用
2.2.3.2四大组件的实现原理
  • Activity 采用宿主manifest中占坑的方式来绕过系统校验,然后再加载真正的activity;
  • Service 动态代理AMS,拦截service相关的请求,将其中转给Service Runtime去处理,Service Runtime会接管系统的所有操作;
  • Receiver 将插件中静态注册的receiver重新注册一遍;
  • ContentProvider 动态代理IContentProvider,拦截provider相关的请求,将其中转给Provider Runtime去处理,Provider Runtime会接管系统的所有操作。
2.3RePlugin
2.3.1特性
特性描述
组件四大组件(含静态Receiver)
升级无需改主程序Manifest完美支持
Android特性支持近乎所有(包括SO库等)
TaskAffinity & 多进程支持(坑位方案)
插件类型支持自带插件(自识别)、外置插件
插件间耦合支持Binder、Class Loader、资源等
进程间通讯支持同步、异步、Binder、广播等
自定义Theme & AppComat支持
DataBinding支持
安全校验支持
资源方案独立资源 + Context传递(相对稳定)
Android 版本API Level 9+ (2.3及以上)
2.3.2架构

![[Android插件化(一)技术调研-1.png]]

模块化,组件化,插件化

在技术开发领域,模块化是指分拆代码,即当我们的代码特别臃肿的时候,用模块化将代码分而治之、解耦分层。具体到 android 领域,模块化的具体实施方法分为插件化和组件化。

一套完整的插件化或组件化都必须能够实现单独调试、集成编译、数据传输、UI 跳转、生命周期和代码边界这六大功能。

解耦思想:
控制反转是一种思想,依赖注入是一种设计模式,IoC框架使用依赖注入作为控制反转的方式

模块化粒度更小,更侧重于重用,而组件化粒度稍大于模块,更侧重于业务解耦。
组件化的核心是角色的转换。 在打包时, 是library; 在调试时, 是application。
组件化开发是纵向分层,模块化开发是横向分块。

组件化想要解决的问题:

  1. 实际业务变化非常快,但是工程之前的业务模块耦合度太高,牵一发而动全身.
  2. 对工程所做的任何修改都必须要编译整个工程
  3. 功能测试和系统测试每次都要进行.
  4. 团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率.
  5. 不能灵活的对工程进行配置和组装.比如今天产品经理说加上这个功能,明天又说去掉,后天在加上.

组件开发比较常见的问题是业务组件的相互引用,为此我们可以通过路由/总线的方式去处理,挂载到组件总线上的业务组件,都可以实现双向通信.而通信协议和HTTP通信协议类似,即基于URL的方式进行.

相对于组件化开发主要要解决的问题:

  1. 宿主和插件分开编译
  2. 并发开发
  3. 动态更新插件
  4. 按需下载模块
  5. 方法数或变量数爆棚

插件化组件化的区别:

  1. 组件化的单位是组件(module);插件化的单位是apk(一个完整的应用)。
  2. 组件化实现的是解耦与加快编译, 隔离不需要关注的部分;插件化实现的也是解耦与加快编译,同时实现热插拔也就是热更新。
  3. 组件化的灵活性在于按加载时机切换,分离出独立的业务组件,比如微信的朋友圈;插件化的灵活性在于是加载apk, 完全可以动态下载,动态更新,比组件化更灵活。
  4. 组件化能做的只是, 朋友圈已经有了,我想单独调试,维护,和别人不耦合,但是和整个项目还是有关联的;插件化可以说朋友圈就是一个app, 我需要整合了,把它整合进微信这个大的app里面

其实从框架名称就可以看出: 组 和 插。
组本来就是一个系统,你把微信分为朋友圈,聊天, 通讯录按意义上划为独立模块,但并不是真正意义上的独立模块。
插本来就是不同的apk, 你把微信的朋友圈,聊天,通讯录单独做一个完全独立的app, 需要微信的时候插在一起,就是一个大型的app了。
插件化的加载是动态的,这点很重要,也是灵活的根源。

所谓架构,无非两个方面: 分层和通信方式。 其实广义的架构也可以说是这两个方面:子模块(子系统)划分和通信。

子模块划分
除了大家公认的common部分, 业务模块的划分尤为重要,相比于狭义上的架构,广义上的子系统的划分的关注点,很考验技术经验以及对业务的理解。

通信方式
模块化的通信方式,无非是相互引入;我抽取了common, 其他模块使用自然要引入这个module
组件化的通信方式,按理说可以划分为多种,主流的是隐式和路由。隐式的存在使解耦与灵活大大降低,因此路由是主流
插件化的通信方式,不同插件本身就是不同的进程了。因此通信方式偏向于Binder机制类似的进程间通信
移动端目前的架构,差异化在于通信机制。通过以上说明,通信机制主要分为3种:

  • 对象持有
  • 接口持有
  • 路由
    通信方式中,对象持有是比较原始的,解耦率最低,建议放弃; 接口持有是个不错的选择,极大程度上实现解耦的诉求,但是解耦不彻底,相互持有交互方的接口。 路由机制也是个不错的选择,可以实现完全解耦,就像组件化一样。但是路由机制的设计是个技术难点,怎么设计效率最高?更健壮?代码可查阅性更好?这些都是值得思考的问题。对于路由机制的优化,阿里的ARouter(用于组件通信)中,采用了分组的模式,我们可以采用;其次可以根据AnnotationProcessor的处理,为每一个注册接收器的组件实现一个SupportActions来确保消息只发送给注册了指定类型的模块,也是个不错的选择。

标签:patchfile,插件,APK,env,组件,Android,public,调研
From: https://blog.csdn.net/sjw890821sjw/article/details/142304725

相关文章

  • android HandlerThread post后 7s才执行
    在Android中,HandlerThread是用来创建一个具有Looper的线程,这样可以在该线程上处理消息和运行任务。当你在HandlerThread上使用Handler的post()方法发送一个Runnable任务时,这个任务会被添加到MessageQueue中,并且会在Looper的主循环中被处理。如果你发现任务在post()之后大约7秒才被......
  • Android13 屏蔽ANR和Crash弹窗
    前言Android系统在应用发生Crash/ANR的时候,总会弹出一个提示对话框,但是现在部分客户不想要这样的对话框,要求移除一、ApplicationCrash表现:程序崩溃或闪退,界面上通常会出现“应用已停止运行”的提示。常见原因(Java异常):错误类型详细描述NullPointerException尝试在需要......
  • vim8 自带插件管理系统
    vim8自带插件管理系统,使用了指定目录的方式来安装插件。在linux下的目录为:~/.vim/pack/自定义目录名/{start,opt}举例说明,比如我安装一个python代码格式化的插件,地址是:https://github.com/Vimjas/vim-python-pep8-indent转到.vim目录后,首先创建pack目录:mkdirpack转......
  • 【Android】ToolBar,滑动菜单,悬浮按钮和可交互提示等的使用方法
    ToolBarToolbar的强大之处在于,它不仅继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件来完成一些MaterialDesign的效果。任何一个新建的项目,默认都是会显示ActionBar。可以打开AndroidManifest看一下:<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns......
  • 我使用本地windows11上的VSCode远程连接到ubuntu进行RUST程序开发,我在VSCode上安装了
    当你使用VSCode的Remote-SSH扩展从本地Windows11连接到远程的Ubuntu服务器进行开发时,插件的安装有以下行为:插件的安装位置本地插件:某些插件,例如VSCode的界面插件或与本地编辑器相关的插件,安装在你的本地Windows系统上。这些插件不需要与远程服务器交互,因此它们仅......
  • 如何在Android上实现RTSP服务器
    技术背景在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的rtspurl,然后把摄像头麦克风甚至屏幕采集的数据,共享出去,轻量级RTSP的设计理念脱颖......
  • 如何在Android上实现RTSP服务器
    技术背景在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的rtspurl,然后把摄像头麦克风甚至屏幕采集的数据,共享出去,轻量级RTSP的设计理念......
  • 免费还超快,我用 Cursor 做的“汉语新解”插件开源了
    前两天,你是否也被&nbsp;汉语新解&nbsp;卡片刷屏,却苦恼于无法快速生成?记得当时,微信群里、朋友圈里、某书上以及公众号里,到处都在谈论这些生动有趣的“汉语新解”卡片。这是由提示词大神@李继刚老师&nbsp;在Claude3.5上开发的提示词。其辛辣的风格和全新的视角,令人耳目一新......
  • 水果软件21更新!Image-Line FL Studio Producer Edition v24.1.1.4285 WIN全插件版本+
    在数字音乐创作领域,FLStudio(也称为FruityLoops)一直以来都是众多音乐制作人心目中的首选工具。随着版本的不断更新迭代,FLStudioProducerEdition24.1.1.4285中文版的发布,无疑为广大的华语音乐创作者们带来了更为便捷、高效且功能强大的音乐制作体验。本文将从多个角度深入......
  • Android Content Provider
    AndroidContentProviderContentProvider是Android中的一种组件,用于管理应用间的数据共享。它允许一个应用将其数据暴露给其他应用,也可以从其他应用中读取数据。通过ContentProvider,应用程序可以更方便地管理数据存储和数据访问,并且支持标准的数据库操作。ContentProvi......