前言
通过前面的学习,相信大家对基本的JNI流程已经有不错的认知了。如果还是有问题,可以看之前的博客。
Android Studio NDK开发(一):准备工作(CMake方式)
Android Studio NDK开发(二):Welcome to JNI
Android Studio NDK开发(三):属性访问
Android Studio NDK开发(四):方法访问
Android Studio NDK开发(五):数组的同步问题
Android Studio NDK开发(六):JNI引用
Android Studio NDK开发(七):文件的加密与解密
本篇博客将介绍文件的拆分与合并。
准备工作
1.创建FilePatchActivity
,别忘记在清单文件中配置,布局文件如下,简单的两个按钮,分别控制拆分与合并
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="拆分"
android:onClick="mSplit"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="合并"
android:onClick="mMerge"/>
</LinearLayout>
2.在cpp文件夹下创建一个C的文件file_patch.c
,然后在CMakeLists.txt中添加库并绑定库,如果对此不明白的,可以参考我之前的博客。
3.创建一个Java类FilePatchUtil
,里面定义两个方法,分别为split
和merge
,同时写上同步代码块,加载file_patch
库。
public class FilePatchUtil {
static {
System.loadLibrary("file_patch");
}
//拆分
public native static void split(String path, String pathPattern, int count);
//合并
public native static void merge(String pathMerge, String pathPattern, int count);
}
4.此时我们可以Alt + Enter一键生成C的函数,此时如果你的项目cpp文件夹下有多个C/C++文件,生成的函数可能不在你所希望的文件下,这时候直接复制粘贴即可。(暂时我还没有更好的处理方式)
拆分
首先添加头文件:
#include "jni.h"
#include <stdlib.h>
#include <stdio.h>
#include <android/log.h>
添加公共的函数:
//根据文件的路径,获得文件的大小
long get_file_size(char* path) {
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
return ftell(fp);
}
接下来就是文件拆分函数:
JNIEXPORT void JNICALL
Java_com_zhangpan_myjnicmake_FilePatchUtil_split(JNIEnv *env, jclass jcla, jstring path_,
jstring path_pattern_, jint file_count) {
//需要分割的文件路径
const char* path = (*env)->GetStringUTFChars(env, path_, NULL);
//分割后的子文件路径模板
const char* path_pattern = (*env)->GetStringUTFChars(env, path_pattern_, NULL);
//得到分割后的子文件的路径列表
char** patches = malloc(sizeof(char*) * file_count);
//也可以这样去动态分配内存
//char **patches = malloc(sizeof(char) * get_file_size(path));
int i = 0;
for (; i < file_count; i++) {
//元素赋值
patches[i] = malloc(sizeof(char) * 100);
//也可以这样动态分配内存
//patches[i] = malloc(sizeof(char) * (get_file_size(path) / file_count));
//path_pattern /storage/emulated/0/AS_%d.avi
// /storage/emulated/0/AS_1.avi
// /storage/emulated/0/AS_2.avi
// /storage/emulated/0/AS_3.avi
// /storage/emulated/0/AS_4.avi
sprintf(patches[i], path_pattern, (i+1));
LOGD("%s", patches[i]);
}
//不断读取path文件,循环写入file_count个文件中
//整除:文件大小为90,分成9个文件,每个文件为10
//不整除:文件大小为110,分成9个文件,前(9-1)个文件为(110 / (9-1)) = 13,最后一个文件为(110 % (9-1)) = 6
//前(9-1)个文件为110/9 + 1 = 13,最后一个文件为 110 - 13*8 = 6
int fileSize = get_file_size(path);
FILE* fpr = fopen(path, "rb");
if (fileSize % file_count == 0) { //整除
int partSize = fileSize / file_count;
int i = 0;
for (; i < file_count; i++) {
int j = 0;
FILE* fpw = fopen(patches[i], "wb");
for(; j < partSize; j++) {
fputc(fgetc(fpr), fpw);
}
fclose(fpw);
}
} else { //非整除
int prePartSize = fileSize / file_count + 1;
int lastPartSize = fileSize - (file_count - 1) * prePartSize;
//写入前(file_count - 1)个文件
int i = 0;
for(; i < file_count - 1; i++) {
FILE* fpw = fopen(patches[i], "wb");
int j = 0;
for (; j < prePartSize; j++) {
fputc(fgetc(fpr), fpw);
}
fclose(fpw);
}
//写入最后一个文件
int j = 0;
FILE* fpw = fopen(patches[file_count - 1], "wb");
for (; j < lastPartSize; j++) {
fputc(fgetc(fpr), fpw);
}
fclose(fpw);
}
fclose(fpr);
//释放
i = 0;
for (; i < file_count; i++) {
free(patches[i]);
}
free(patches);
(*env)->ReleaseStringUTFChars(env, path_, path);
(*env)->ReleaseStringUTFChars(env, path_pattern_, path_pattern);
}
分配原理图如下:
1.png首先将分割后的子文件路径作为二维数组的成员,分别动态给这个二维数组和成员分配内存。
定义一个path_pattern
字符串:/storage/emulated/0/AS_%d.avi
,%d
通过sprintf
函数动态替换成1, 2, 3, 4,从而将其作为分割后的子文件路径名称。
当不能整除时,有两种方法可做处理,我所采用的是第二种分配方法,第一种分配方法可能导致最后一个文件没有任何内容(如文件大小为104,分成9个文件),但是仍然可以合并。
合并
JNIEXPORT void JNICALL
Java_com_zhangpan_myjnicmake_FilePatchUtil_merge(JNIEnv *env, jclass jcla, jstring path_merge_,
jstring path_pattern_, jint file_count) {
const char *path_merge = (*env)->GetStringUTFChars(env, path_merge_, NULL);
const char *path_pattern = (*env)->GetStringUTFChars(env, path_pattern_, NULL);
char** patches = malloc(sizeof(char*) * file_count);
FILE* fpw = fopen(path_merge, "wb");
int i = 0;
for (; i < file_count; i++) {
patches[i] = malloc(sizeof(char) * 100);
sprintf(patches[i], path_pattern, (i+1));
// __android_log_print(ANDROID_LOG_DEBUG, "zp", "%s", patches[i]);
LOGD("%s", patches[i]);
//获取每个分割后的子文件大小
int patchFileSize = get_file_size(patches[i]);
FILE* fpr = fopen(patches[i], "rb");
//边读边写
int j = 0;
for (; j < patchFileSize; j++) {
fputc(fgetc(fpr), fpw);
}
fclose(fpr);
}
fclose(fpw);
//释放
i = 0;
for(; i < file_count; i++) {
free(patches[i]);
}
free(patches);
(*env)->ReleaseStringUTFChars(env, path_merge_, path_merge);
(*env)->ReleaseStringUTFChars(env, path_pattern_, path_pattern);
}
文件合并的过程,其实就是文件拆分过程的逆过程,相对来说更加简单,不需要考虑是否整除。
由于需要在Android Studio中打印log日志,添加了头文件
#include <android/log.h>
比如代码中的log:
__android_log_print(ANDROID_LOG_DEBUG, "zp", "%s", patches[i]);
此时就可以打log日志并且可以在Android Studio的Logcat日志中查看。但是每次这样写过于繁琐,我们可以通过宏定义来简化代码:
#define LOGD(FORMAT, ...) __android_log_print(ANDROID_LOG_DEBUG, "zp", FORMAT, ##__VA_ARGS__)
此时我们可以这样调用:
LOGD("%s", patches[i]);
FilePatchActivity中添加按钮点击方法,分别开启线程处理:
public class FilePatchActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_file_patch);
}
public void mSplit(View view) {
new Thread(new Runnable() {
@Override
public void run() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "AS.avi";
String pathPattern = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "AS_%d.avi";
FilePatchUtil.split(path, pathPattern, 4);
Log.d("zp", "拆分完成...");
}
}).start();
}
public void mMerge(View view) {
new Thread((new Runnable() {
@Override
public void run() {
String pathMerge = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "AS_merge.avi";
String pathPattern = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "AS_%d.avi";
FilePatchUtil.merge(pathMerge, pathPattern, 4);
Log.d("zp", "合并完成...");
}
})).start();
}
}
运行
清单文件添加读写权限,如果在6.0以上,需要动态申请权限,也可以手动手机中对应apk的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
点击运行,点击拆分按钮,可在Logcat中查看
2.png由于我的Android Studio是3.0的,新增了Device File Explorer(默认在Android Studio的右下角),可以很轻松地查看程序执行后的文件目录,如下图:
3.png可以看到这里生成了四个文件,我Debug发现,这里并不是整除,只不过最后一个文件大小和其他的相差不大,换算成MB,也就近似相等。
再点击合并按钮,可在Logcat中查看
4.png验证
打开手机,找到对应的AS_merge.avi,点击播放,发现可以完整观看,说明合并完成。
项目地址
https://github.com/fsrmeng/MyJniCmake-Master
展望
关于JNI流程系列文章即将结束,接下来我们将介绍NDK开发的技术,敬请期待!
喜欢本篇博客的简友们,就请来一波点赞,您的每一次关注,将成为我前进的动力,谢谢!
网友评论