正常一个项目的版本更新,很多情况下是进行apk包的新版本发布,让用户下载更新,但是有个弊端就是如果包体很大,这样就耗时又费流量。
Apk文件的拆分和合并需要用bsdiff和bzip2这两个工具
bsdiff-win文件.png
release目录是现成的拆分和合并的可执行文件可以通过命令,直接进行拆分和合并
可执行文件.png
Apk的文件拆分,将新版本的apk和旧版本的apk,差异的内容进行分解出来,生成.patch文件
拆分命令.png
cmd命令:
bsdiff.exe appOld.apk appNew.apk apk.patch
命令行说明:
第一个是拆分的可执行的文件名
第二个是旧文件的名称
第三个是新文件的名称
第四个是拆分(.patch)文件名
工程源码.png
直接看bsdiff.cpp文件的源码:
int main(int argc,char *argv[])
{
..............省略代码................
if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
...........省略代码................
}
有个main方法,如果直接编译是可以生产exe文件,这边将main方法进行修改为bsdiff_main,采用jni的形式进行调用
以静态的native注册为例, 关于native的动态注册,可以参考https://cloud.tencent.com/developer/article/1645168
JNIEXPORT void JNICALL Java_jason_DiffUtil_diffApk
(JNIEnv *env, jclass jclz, jstring oldPath_jst, jstring newPath_jst, jstring patchPath_jst) {
char * oldPath = (char*)env->GetStringUTFChars(oldPath_jst, NULL);
char * newPath = (char*)env->GetStringUTFChars(newPath_jst, NULL);
char * patchPath = (char*)env->GetStringUTFChars(patchPath_jst, NULL);
int argc = 4;
char *argv[4];
argv[0] = "bsdiff";
argv[1] = oldPath;
argv[2] = newPath;
argv[3] = patchPath;
bsdiff_main(argc, argv);
env->ReleaseStringUTFChars(oldPath_jst, oldPath);
env->ReleaseStringUTFChars(newPath_jst, newPath);
env->ReleaseStringUTFChars(patchPath_jst, patchPath);
}
新建的项目工程默认都是生成exe,修改下输出类型,属性---->常规---->项目默认值:配置类型
修改输出类型.png
默认打出来的dll包是32位的,如果是64的系统环境,修改一下项目配置,vs工具栏--->生成--->配置管理器,如下图:
修改方案平台.png
最后生成解决方案
生成Dll.png
注:vs 切换平台之后 需要重新配置上述依赖
项目结构图.png
public class DiffUtil {
static{
System.loadLibrary("BsDiffUtil");
}
public static native void diffApk(String oldPath, String newPath, String patch);
}
public class MainTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
String oldPath = "F:\\Test\\appNew.apk";
String newPath = "F:\\Test\\appOld.apk";
String patch = "F:\\Test\\apk.patch";
System.out.println("bsdiff start");
DiffUtil.diffApk(oldPath, newPath, patch);
System.out.println("bsdiff end");
}
}
文件的拆分就先介绍到这。
文件的合并,指的是旧的Apk文件合并.patch文件,成为新的Apk文件。
采用Android studio项目为例,来处理客户端的的文件合并
bsdiff的源码.png
同样需要bzip2的源代码:
bzip2的源码文件.png
将bsdiff的bspatch.c文件和bzip2的.c和.h文件copy到项目的cpp目录下(为了将bzip和bspatch分开,单独创建个目录存放)
as下的cpp文件.png
cmake_minimum_required(VERSION 3.4.1)
file(GLOB my_c_path bzip2/*.c)
add_library( # Sets the name of the library.
my-test
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${my_c_path}
bspatch.c)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
my-test
# Links the target library to the log library
# included in the NDK.
${log-lib} )
public class BsPatch {
public native static int patch(String oldfile, String newFile, String patchFile);
}
JNIEXPORT jint JNICALL
Java_com_ndk_so_generator_BsPatch_patch(JNIEnv *env, jclass clazz, jstring oldfile,
jstring new_file, jstring patch_file) {
int ret= -1;
LOGD(" jni patch begin");
char *oldPath = (char *) (*env)->GetStringUTFChars(env, oldfile, JNI_FALSE);
char *newPath = (char *) (*env)->GetStringUTFChars(env, new_file, JNI_FALSE);
char *patchPath = (char *) (*env)->GetStringUTFChars(env, patch_file, JNI_FALSE);
int argc = 4;
char *argv[4];
argv[0] = "TimBsPatch";
argv[1] = oldPath;
argv[2] = newPath;
argv[3] = patchPath;
//如果成功,ret等于0
ret = bspatch_main(argc,argv);
(*env) -> ReleaseStringUTFChars(env, oldfile, oldPath);
(*env) -> ReleaseStringUTFChars(env, new_file, newPath);
(*env) -> ReleaseStringUTFChars(env, patch_file, patchPath);
}
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager()
.getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
MainActivity的代码:
public class MainActivity extends AppCompatActivity {
public static final String TAG = "chenby";
private static int REQ_PERMISSION_CODE = 1001;
private static final String[] PERMISSIONS = { Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("my-test");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
checkAndRequestPermissions();
}
private void init() {
TextView tv = findViewById(R.id.sample_text);
if (ApkUtils.getVersionCode(this, getPackageName()) < 2.0) {
tv.setText("不是最新的版本号 开始更新 ");
new ApkUpdateTask().execute();
} else {
tv.setText(" 最新版本号 无需更新");
}
}
/**
* 权限检测以及申请
*/
private void checkAndRequestPermissions() {
// Manifest.permission.WRITE_EXTERNAL_STORAGE 和 Manifest.permission.READ_PHONE_STATE是必须权限,允许这两个权限才会显示广告。
if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
&& hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
init();
} else {
ActivityCompat.requestPermissions(this, PERMISSIONS, REQ_PERMISSION_CODE);
}
}
/**
* 权限判断
* @param permissionName
* @return
*/
private boolean hasPermission(String permissionName) {
return ActivityCompat.checkSelfPermission(this, permissionName)
== PackageManager.PERMISSION_GRANTED;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQ_PERMISSION_CODE) {
checkAndRequestPermissions();
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void onOpen(View view) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
class ApkUpdateTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
String newFile = Contants.NEW_APK_PATH;
String patchFileString = Contants.PATCH_FILE_PATH;
File patchFile = new File(patchFileString);
if(!patchFile.exists()) {
return false;
}
Log.d(TAG,"开始合并");
int ret = BsPatch.patch(oldfile, newFile,patchFileString);
Log.d(TAG,"开始完成");
if (ret == 0) {
return true;
} else {
return false;
}
}
@Override
protected void onPostExecute(Boolean aBoolean) {
if (aBoolean) {
Log.d(TAG,"合并成功 开始安装新apk");
ApkUtils.installApk(MainActivity.this, Contants.NEW_APK_PATH);
}
}
}
}
以上就是一个简单的增量更新过程:主要的内容是在服务端对apk文件进行拆分出(.patch)文件,然后再客户端将旧版本apk和服务端下载下来(.patch)进行合并出新版本apk,进行新版本安装更新。
项目源码
https://github.com/jasonkevin88/BsDiffUtil