今天实现一种不通过应用市场进行更新的方式,可以动态更新,类似于某些app
的检查更新功能。
用户点击检查更新,会先下载新包,下载完毕后重新安装app
,覆盖旧的app
,实现更新。
先介绍一个差分库,bsdiff
他能计算两个文件的不同并生成差分文件,也可以通过差分文件进行合并,生成新文件
使用:
现在在Windows
平台上实现一个old.dex
的版本升级(旧文件升级)的功能
http://www.daemonology.net/bsdiff/
这里解释一下,cmd
运行的就是.exe
文件,cmd
去执行的时候就是执行的他们的main
函数,后边拼接的参数也就是我们main
函数需要的参数。
切换到包含bsdiff.exe
和bspatch.exe
的目录,cmd
敲入下面命令
bsdiff old.dex new.dex patch
//参数1 文件1路径
//参数2 文件2路径
//参数3 生成的差分文件路径
这其实就是就是执行了bsdiff.c
的main
函数,传入两个参数分别是
//4, bsdiff,old.dex,new.dex,patch
int main(int argc, char * argv[])
{
}
还是当前目录,cmd
敲入下面命令
bspatch old.dex new2.dex patcth
//参数1 源文件路径
//参数2 生成的新文件路径
//参数3 差分文件路径
此时在当前目录生成了new2.dex
此时这个文件和new.dex
是等价的
使用010Editor
查看两个文件
new.dex
发现两个文件一毛一样
这是我们在Windows
下的操作
如果想在android
使用呢?
现在实现文章开头说的需求
创建项目时我们选择c++
,生成的项目结构如下
project视图
android视图
只是多了cpp
目录
CMakeList.text
类似于于我们的gradle
,是c++
的构建文件
native-lib
是系统帮我们生成的jni
调用,里面有一个默认的函数
要知道的是,客户端不用去做diff
,patch
是服务器下发的,也就是说我们不需要bsdiff.c
,只需要 bspatch.c
就可以了,复制我们的bspatch.c
到cpp
目录下
修改cmake
文件,导入我们的bspatch.c
add_library( # Sets the name of the library.
mydexdiff
# Sets the library as a shared library.
SHARED
bspatch.c
# Provides a relative path to your source file(s).
native-lib.cpp)
此时发现bspatch.c
存在爆红
此处需要导入bzlib.h
,下载地址https://sourceforge.net/projects/bzip2/
接下来导入bzlib
,复制文件到cpp
目录
接下来在cmake
添加bzip
依赖,bzip
的文件比较多,因此不适合在add_library
中一个一个添加,我们使用下面的方法
#声明SOURCES变量,代表bzip2的文件夹
aux_source_directory(bzip2 SOURCES)
add_library(
bspatch_utlis
SHARED
native-lib.cpp
bspatch.c
#导入SOURCES
${SOURCES})
#引入bzip2文件夹
include_directories(bzip2)
这时候bspatch
不再爆红
修改一下生成的so库
的名称
target_link_libraries( # Specifies the target library.
#so库名称
bspatch_utlis
# Links the target library to the log library
# included in the NDK.
${log-lib})
add_library( # Sets the name of the library.
bspatch_utlis
#省略其他
)
修改app
的build.gradle
,在defaultConfig
中添加
//根据不同的cpu架构,编译不同so库的版本,x86基本时模拟器的架构
ndk{
abiFilters 'x86','arm64-v8a'
}
//配置打包进apk的so库版本
externalNativeBuild{
cmake{
abiFilters 'x86','arm64-v8a'
}
}
编译查看生成的apk
,下面就是我们新的apk
目录结构
在MainActivity
中声明native
方法
public static native int patch(String oldApk,String newApk,String patchFile);
alt+Enter
自动生成jni
方法调用
会在native-lib.cpp
中生成
extern "C"
JNIEXPORT jint JNICALL
Java_com_hbsf_mydexdiff_MainActivity_patch(JNIEnv *env, jclass clazz, jstring old_apk,
jstring new_apk, jstring patch_file) {
// TODO: implement patch()
}
现在需要调用c
代码,需要在cpp
中进行适配
//这样声明时告诉当前文件有一个地方实现了executePatch,你自己去找吧,所以我们也要把bspatch的main改为executePatch
extern "C" {
extern int executePatch(int argc, char *argv[]);
}
完善上面的方法
Java_com_hbsf_mydexdiff_MainActivity_patch(JNIEnv *env, jclass clazz, jstring old_apk,
jstring new_apk, jstring patch_file) {
//之前说过了,调用bspatch需要4个参数
int args = 4;
char *argv[args];
argv[0] = "bspatch";
//必须把java的字符串转换成cpp层的字符串
argv[1] = (char *) (env->GetStringUTFChars(old_apk, 0));
argv[2] = (char *) (env->GetStringUTFChars(new_apk, 0));
argv[3] = (char *) (env->GetStringUTFChars(patch_file, 0));
//此处executePathch()就是上面我们修改出的
int result = executePatch(args, argv);
//手动进行释放
env->ReleaseStringUTFChars(old_apk, argv[1]);
env->ReleaseStringUTFChars(new_apk, argv[2]);
env->ReleaseStringUTFChars(patch_file, argv[3]);
__android_log_print(ANDROID_LOG_ERROR,"diff","==%s==%s==%s==%d",argv[1] ,argv[2] ,argv[3],result );
return result;
}
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.hbsf.mydexdiff.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
provider>
public void patch() {
//生成新apk的路径
File newFile = new File(getExternalFilesDir("apk"), "app.apk");
//patch的路径,我么们给他放在私有目录下了
File patchFile = new File(getExternalFilesDir("apk"), "patch.apk");
Log.e("patch", getApplicationInfo().sourceDir + "\n" + newFile.getAbsolutePath() + "\n" + patchFile.getAbsolutePath());
//调用native方法
int result = patch(getApplicationInfo().sourceDir, newFile.getAbsolutePath(),
patchFile.getAbsolutePath());
if (result == 0) {
//安装apk
install(newFile);
}
}
//安装代码
private void install(File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 7.0+以上版本
Uri apkUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
startActivity(intent);
}
运行则可以安装新的apk
,发现和我我们修改的地方一样
之前
之后
bsdiff
是根据整体文件的差分,不稳定,有时候很快有时候很慢,虽然说这是后台帮我们做的事情,但是客户端也要了解。
每种文件都有他的结构,我们可以通过了解dex
的结构,只对某些需要改动的结构进行修改,这样就充分利用了dex
的特性,进行了优化。
首先是jar
和dex
的区别,dex
会把每个类的字符串只保存一份在字符串索引区,而jar
包时每个class
自己保存自己的,相当于dex
进行了压缩优化
old_test.java
如下
public class Test {
public static void test() {
System.out.println("a1");
System.out.println("b1");
System.out.println("c1");
System.out.println("d1");
System.out.println("e1");
System.out.println("f1");
System.out.println("g1");
System.out.println("h1");
System.out.println("i1");
}
}
把生成的dex
导入010Editor
一共21个字符串包括的a1,b1
之类的字符串
存储的是偏移地址和大小,表示一个字符串在文件存储的位置和区域
上面我们使用的是bsdiff
,全部的文件都可以使用bsdiff
进行差分,并没用使用dex文件的特性,这里提供dex修改的思路
通过010我们可以清楚的看到dex
的文件内部结构,所以可以通过解析这个二进制文件,取出来所有的数据,根据不同的区封装成不同的对象,比如头区,字符串区等等,解析完旧的dex
再解析新的dex
,对两个dex
的对象属性进行对比,可以把这些差异持久化保存下来,按照自己的规则协议来生成,在合并的时候就可以通过解析这个差分文件,这个差分文件的基本信息就是在哪需要操作,操作是什么,改变的数据是什么,再封装一个自己的dex
文件输出流,就可以完美的把旧的dex
同通过差分文件变成新的dex
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!