1 | // C++ 的标准输入,输出头文件 |
C++中也是存在内部类的,而且内部类的作用和使用与命名空间非常类似;
1 | class Err{ |
1 | // C++ 的标准输入,输出头文件 |
C++中也是存在内部类的,而且内部类的作用和使用与命名空间非常类似;
1 | class Err{ |
1,下载bsdiff源码:(bsdiff4.3-win32-src.zip)
http://www.daemonology.net/bsdiff/
2,创建一个新的C项目,将源码中的C和C++文件拷贝进来;1
2暂时只需要bsdiff.cpp,用来拆分新包,bspatch.cpp不可以引入;
bspatch是用来合并包文件的,需要引入bzip2的源码来辅助操作,如果引入将会报错!!!
3,关于_CRT_SECURE_NO_WARNINGS 错误:1
2
3
4
5由于项目中可能使用了老版本、非安全版本的函数,所以编译时将会报错,解决方案是继续使用这些
函数。
另一个问题是,如果存在多个这种情况的源文件时,可以通过在命令行中输入:
-D _CRT_SECURE_NO_WARNINGS
4,报错:warning C4996: ‘setmode’: The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _setmode. See online help for details.
1 | 原因与上面类似,可以通过增加宏定义来消除警告:_CRT_NONSTDC_NO_DEPRECATE |
5,error C4703: 使用了可能未初始化的本地指针变量“_new”
1 | 由于源码使用了不同系统的编译器,而visual studio默认对语法进行了严格的检查,所以会出现以上的警告,解决方案是关闭掉vs的检查; |
如果生成解决方案成功,则说明配置成功,可以开始进行jni配置;
6,可以看到在bsdiff.cpp中存在启动main函数,通过调用该方法可以生成我们需要的拆分包;
1 | // 将main函数修改为bsdiff_main,方便我们的jni函数调用 |
7,编写jni函数,并生成对应的解决方案:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18JNIEXPORT void JNICALL Java_com_hsh_bsdiff_BsdiffUtill_diff_1apk
(JNIEnv *env, jclass jcls, jstring fileName_, jstring oldFile_, jstring newFile_, jstring patchFile_){
char* fileName = (char*)env->GetStringUTFChars(fileName_, 0);
char* oldFile = (char*)env->GetStringUTFChars(oldFile_, 0);
char* newFile = (char*)env->GetStringUTFChars(newFile_, 0);
char* patchFile = (char*)env->GetStringUTFChars(patchFile_, 0);
char *args[] = { fileName, oldFile, newFile, patchFile};
bsdiff_main(4, args);
env->ReleaseStringUTFChars(fileName_, fileName);
env->ReleaseStringUTFChars(oldFile_, oldFile);
env->ReleaseStringUTFChars(newFile_, newFile);
env->ReleaseStringUTFChars(patchFile_, patchFile);
}
对应的Java方法:
1 | public class BsdiffUtill { |
运行Java程序,可以在对应路径下生成拆分包;
8,合并拆分包;
注意,Android是linux系统,所以在编写NDK时,bsdiff要使用linux系统版本下的源码。
9,bsdiff合并时,需要使用到bzip2的源码,所以同样需要下载bzip2的源码:
https://download.csdn.net/download/juncojet/10235311
然后引进项目中;
10,让bspatch.c中引用bzip2的源码;
添加头文件引用:1
2
3
4
5
6
7
8
9
10
11
12
13// 本源文件只要求引入该头文件
#include "bzip2/bzlib.c"
// 但实际编译时,缺报bzlib.c缺少对应的引用函数(BZ2_crc32Table,BZ2_compressBlock,BZ2_crc32Table...),
// 大概猜测原因是:在配置CMakeLists时,只配置了bspatch.c文件,而bzip2源文件并没有配置,所以这些文件没有
// 被项目管理起来。在bzlib.c中,头文件bzlib_private.h声明的函数无法被项目知道,所以在bzlib.c进行调用时,
// 才会报出找不到这些函数的错误。解决方案就是引入底下的源文件,不再通过bzlib_private.h找,而是直接引用;
#include "bzip2/crctable.c"
#include "bzip2/compress.c"
#include "bzip2/decompress.c"
#include "bzip2/randtable.c"
#include "bzip2/blocksort.c"
#include "bzip2/huffman.c"
之后按照上面的生成拆分包的步骤,编写Android下的NDK;
11,在bspatch.c下添加jni调用函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21JNIEXPORT void JNICALL
Java_com_hsh_bsdiff_1patch_PatchUtil_patchFile(JNIEnv *env, jclass type, jstring oldFile_,
jstring newFile_, jstring patchFile_) {
char *oldFile = (char *) (*env)->GetStringUTFChars(env, oldFile_, 0);
char *newFile = (char *) (*env)->GetStringUTFChars(env, newFile_, 0);
char *patchFile = (char *) (*env)->GetStringUTFChars(env, patchFile_, 0);
char *fileName = "apk_patch";
char *params[] = {fileName, oldFile, newFile, patchFile};
// 将源文件的main函数直接修改为patch_main,进行调用
patch_main(4,params);
__android_log_print(ANDROID_LOG_INFO, "PatchFile", "oldFile = %s", oldFile);
__android_log_print(ANDROID_LOG_INFO, "PatchFile", "newFile = %s", newFile);
__android_log_print(ANDROID_LOG_INFO, "PatchFile", "patchFile = %s", patchFile);
(*env)->ReleaseStringUTFChars(env, oldFile_, oldFile);
(*env)->ReleaseStringUTFChars(env, newFile_, newFile);
(*env)->ReleaseStringUTFChars(env, patchFile_, patchFile);
}
Java代码:1
2
3
4
5
6
7
8public class PatchUtil {
static{
System.loadLibrary("native-lib");
}
public native static void patchFile(String oldFile,String newFile,String patchFile);
}
12,旧版的apk中需要有合并升级的逻辑,这样在下载拆分包后,可以直接进行合并;
重要的api1
2// 获得应用包下保存的当前版本的apk
final String apkPath = getApplicationContext().getPackageResourcePath();
模拟合并后执行安装:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18new Thread(new Runnable() {
@Override
public void run() {
try {
PatchUtil.patchFile(apkPath, newPath, patch_path);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "success", Toast.LENGTH_LONG).show();
// 7.0以上需要配置FileProvider才能安装
ApkUtils.installApk(MainActivity.this, newPath);
}
});
} catch (Error error) {
error.printStackTrace();
}
}
}).start();
生成新的apk文件的路径可以是任意,即使在安装后被删除也是无所谓的,因为应用包下会自动保有原来的apk文件!
当有新版的apk发布时,利用新版的apk与旧版的apk,调用上面步骤生成拆分库,生成对应的拆分包。
不同版本的旧apk通过访问服务器,获取到对应的升级拆分包,进行升级。
差分算法
差分算法是对新旧的两个文件进行比较,筛选,分类出两个文件中的相同部分,不同部分(修改的部分),以及额外增加的部分;
对于相同的部分,会用下标标识出来,对于不同的部分和额外增加的部分,会通过bzip2进行压缩,
所以生成的差分包 = 映射标识相同位置的数据+不同部分、额外增加部分的压缩数据;
合成时,对于相同的部分,根据差分包中标识的下标,从旧文件中直接读取出来,写进新文件中;对于不同的部分(修改的部分)和额外增加的部分,通过bzip2进行解压,再写进新文件中;
特点,生成差分包时,差分包的大小并不与新、旧包大小差值成正比关系,而是与新旧包中数据的重复情况成正比关系:重复的数据越多,则生成的差分包越小;被修改的部分越多,则对应生成的映射标识越多,差分包也越大;
通过在方法中声明局部的静态变量,可以在方法初始化时进行方法中的某个资源的初始化,
并且在以后每次调用该方法时,确保不需要重复初始化该资源,同时确保其他的方法无法调用
到。
1 | #include <stdio.h> |
Java中调用
1 | public native void cached(); |
全局变量可以在dll库加载时进行进行全局变量的初始化;
1 | #include <stdio.h> |
Java中调用
1 | static { |
jni层抛出的异常是无法在Java层被捕获到,只能在C层进行清空,如果有必要,需要向Java层主动
抛出异常进行警告;
/错误纠正:jni层的异常是可以被Java层捕获到的!!!/
// 根据Jni层抛出的异常,可以发现其实Throwable和Error的子类,所以我们可以通过捕获这两者
// 来达到捕获的目的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24JNIEXPORT void JNICALL Java_com_my_jnitest_JniTest_catchException
(JNIEnv * env, jobject jobj){
jclass cls = (*env)->GetObjectClass(env, jobj);
jfieldID fid = (*env)->GetFieldID(env, cls, "key2", "Ljava/lang/String;");
// 捕获Jni异常
jthrowable exc = (*env)->ExceptionOccurred(env);
if (exc != NULL){
// 清空异常,确保Java层能够运行
(*env)->ExceptionClear(env);
// 重新获取
fid = (*env)->GetFieldID(env, cls, "key", "Ljava/lang/String;");
jstring str = (*env)->GetObjectField(env, jobj, fid);
char* c_str = (*env)->GetStringUTFChars(env, str, NULL);
// 抛出Java层能够捕获的异常
if (_stricmp(c_str, "hsh") != 0){
jclass new_exc = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
(*env)->ThrowNew(env, new_exc, "key's value is invalid!");
}
}
}
java:1
2
3
4
5
6
7try {
// Jni层次的异常是无法被捕获到的
test.catchException();
} catch (Exception e) {
// 我们在Jni中主动抛出的异常能够被捕获
System.out.println(e.getMessage());
}
/通过捕获Throwable进行达到Jni层的异常捕获/1
2
3
4
5
6
7try {
// Jni层次的异常是无法被捕获到的
test.catchException();
} catch (Throwable e) {
// 我们在Jni中主动抛出的异常能够被捕获
System.out.println(e.getMessage());
}
封装InputStream和OutputStream为Source和Sink,提供了一些列高效读写字节和字符的方法;
向File中写入数据
1 | String name = "xiaoming"; |
文件拷贝
1 | try { |
对应原生IO的InputStream,提供字节流数据,通过source,可以从网络、存储、内存等地方读取到数据流;
提供了read方法,支持Sink读取数据到其Buffer缓存中;
一般情况下,不要直接操作Source,而是操作BufferedSource,它提供了更丰富、高效的接口;
Source还提供了一个强大的skip方法(BufferedSource),能让我们跳过指定数量的字节后,再读字节流;
1 | public interface Source extends Closeable { |
带有缓存的Source接口,实际实现是RealBufferedSource;
对应原生IO的OutputStream,提供了write方法,用来从Source缓存中读取缓存数据,写入到自己的
Buffer缓存中;
一般情况下,不要直接操作Sink,而是操作BufferedSink,它提供了更丰富、高效的接口;
Sink能够代替DataOutputStream(写入原生数据)、BufferedOutputStream(写入缓存数据)、
OutputStreamWriter(数据流字符编码);
在结束时,必须调用Sink的close方法,将缓存数据推送进目标中,并且释放调用持有的资源(在采用
链式调用时,需要注意最后也需要关闭Sink);
通过:1
Okio.buffer(Sink);
可以得到BufferedSink实例;
1 | public interface Sink extends Closeable, Flushable { |
带有缓存的Sink接口,实际实现是BufferedSink类;
Segment字面的意思就是片段,okio将数据也就是Buffer分割成一块块的片段,内部维护者固定长度的
byte[]数组,同时segment拥有前面节点和后面节点,构成一个双向循环链表;
属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33/** The size of all segments in bytes. */
static final int SIZE = 8192;
/** Segments will be shared when doing so avoids {@code arraycopy()} of this many bytes. */
// 在调用split分割Segment时,如果要求分割的字节数超过了该值,则不会采用复制数组的方式进行
// 分割,而是采用共享的方式
static final int SHARE_MINIMUM = 1024;
final byte[] data;
// Segment保存的字节数组中,第一个可读的位置
/** The next byte of application data byte to read in this segment. */
int pos;
// Segment的剩余可写的字节数组的第一个位置
// 所以Segment实际保存的数据为pos ~ limit-1
/** The first byte of available data ready to be written to. */
int limit;
// Segment的字节数据是不是与别的Segment共享的,默认情况下是false
// 当在split时采用的是共享的方式,则分割出来的Segment该变量为true
/** True if other segments or byte strings use the same byte array. */
boolean shared;
// 当前Segment是否是其中字节数据的拥有者
/** True if this segment owns the byte array and can append to it, extending {@code limit}. */
boolean owner;
/** Next segment in a linked or circularly-linked list. */
Segment next;
/** Previous segment in a circularly-linked list. */
Segment prev;
pos和limit变量类似于两个指针,在Segment进行字节数据共享时,这两个变量用来指定当前Segment
有效的数据域(范围),这也是为什么,当多个Segment共享字节数据时,Segment无法被修改。如果修改了,
那么其他的Segment的有效数据域可能会受到影响;
创建一个新的Segment,这个与创建它的Segment共享同一个字节数组数据,并且标明这个新的Segment不是
字节数组数据的拥有者;1
2
3
4Segment sharedCopy() {
shared = true;
return new Segment(data, pos, limit, true, false);
}
创建一个全新的Segment,并且这个Segment的数据与创建它的Segment是相同的;1
2
3Segment unsharedCopy() {
return new Segment(data.clone(), pos, limit, false, true);
}
将当前Segment从链表中移除;1
2
3
4
5
6
7
8public @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
将Segment添加到当前Segment之后;1
2
3
4
5
6
7public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
根据传入的字节数,分割Segment,将新分割出来的Segment添加到当前Segment之前;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public Segment split(int byteCount) {
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
// We have two competing performance goals:
// - Avoid copying data. We accomplish this by sharing segments.
// - Avoid short shared segments. These are bad for performance because they are readonly and
// may lead to long chains of short segments.
// To balance these goals we only share segments when the copy will be large.
// 如果分割的字节数超过了1024,则采用共享字节数组的方式
if (byteCount >= SHARE_MINIMUM) {
prefix = sharedCopy();
} else {
// 否则直接创建一个新的Segment,并复制数据
prefix = SegmentPool.take();
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
// 如果采用共享数据的方式,data数据拥有者的pos最大不会超过limit
// 如果是共享数据的方式,分割后的两个Segment实际是通过pos和limit来分别指向同一个字节数组的
// 不同数据段
prefix.limit = prefix.pos + byteCount;
// 当前Segment的pos重新指向
pos += byteCount;
// 将分割出去的Segment放到前一个Segment的后面,也就是当前Segment的前面
prev.push(prefix);
return prefix;
}
合并当前Segment和它之前的Segment为一个;1
2
3
4
5
6
7
8
9
10
11
12
13
14public void compact() {
if (prev == this) throw new IllegalStateException();
if (!prev.owner) return; // Cannot compact: prev isn't writable.
int byteCount = limit - pos;
// 如果prev是与别的Segment共享字节数组的,则其只能使用从limit~SIZE的部分
// 否则,0~pos部分也是可以使用的,当然,一般情况下,pos是等于0的
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
// 开始合并到prev中
writeTo(prev, byteCount);
// 移除当前Segment
pop();
SegmentPool.recycle(this);
}
将指定字节数的数据移到Sink中;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public void writeTo(Segment sink, int byteCount) {
if (!sink.owner) throw new IllegalArgumentException();
// sink从limit~SIZE的部分不足以放入当前Segment,则检查Sink的0~pos部分是否可以放入数据
if (sink.limit + byteCount > SIZE) {
// We can't fit byteCount bytes at the sink's current position. Shift sink first.
// 虽然Sink是其字节数组的拥有者,但是其字节数组一部分是与别人共享的,这部分共享的数据,
// 可能使用了0~pos的部分,所以这部分不能被修改
if (sink.shared) throw new IllegalArgumentException();
// 加上前面的pos部分也不足以将当前Segment数据移进Sink中
if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
// 将sink的字节数组数据移到pos = 0的位置
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
// 将当前Segment的数据移到Sink中
System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
pos += byteCount;
}
ByteString内部可以保存byte类型的数据,作为一个工具类,它可以把byte转为String,这个String可
以是utf8的值,也可以是base64后的值,也可以是md5的值等等;