Android NDK 開發:實戰案例(轉)

轉自:http://cfanr.cn/2017/08/19/Android-NDK-dev-practice-samples/
0. 前言

如果只學理論,不做實踐,不踩踩坑,一般很難發現真正實踐項目中的問題的,也比較難以加深對技術的理解。所以延續上篇 JNI 的實戰Android NDK開發:JNI實戰篇 ,這篇主要是一些 NDK 小項目的練習,由於這些項目網上都有 demo介紹,這裏不會具體一步步介紹如何操作,只記錄一些個人需要注意的地方或一些主要步驟,詳細的介紹或代碼可以點擊裏面的鏈接查看。

1. 文件加解密和分割合併

1.1 簡介

所有文件都是二進制存儲的,無論是文本,圖片還是視頻文件都是以二進制存儲在磁盤中。所以可以通過對文件進行二進制運算進行加解密。下面用到的是比較簡單的^異或運算來對文件加解密(算是一種對稱加密算法)
附:加解密算法擴展:加解密算法 · 區塊鏈技術指南

一般在大文件傳輸時,如音視頻文件,會將文件分割後再傳輸,從而提高效率。當需要使用時,再將分割後的文件合併即可。

而文件加解密設計到安全,可以使用 NDK 增加反編譯的難度。另外文件的分割合併都比較耗性能,可以放到 NDK 處理提高效率。

以下練習參考:NDK開發基礎②文件加密解密與分割合併 - 簡書

效果圖如圖,進入界面會拷貝兩張 assets 的圖片cats.jpgimage.jpg到本地 sdcard/NdkSample 目錄下作爲測試。加密後的圖片 cats_encypt.jpg是無法直接查看的,合成的圖片是 image.jpeg
效果圖

1.2 文件加解密

Java 代碼

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
public class FileUtils {
private static final String FILE_PATH_PREFIX = Environment.getExternalStorageDirectory() + File.separator;
private static final String FOLDER_NAME = "NdkSample" + File.separator;
public static final String FILE_PATH = FILE_PATH_PREFIX + FOLDER_NAME;
public static boolean fileEncrypt() {
String normalFilePath = FILE_PATH + "cats.jpg";
String encryptFilePath = FILE_PATH + "cats_encrypt.jpg";
try {
return fileEncrypt(normalFilePath, encryptFilePath);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public static boolean fileDecode() {
String encryptFilePath = FILE_PATH + "cats_encrypt.jpg";
String decodeFilePath = FILE_PATH + "cats_decode.jpg";
try {
return fileDecode(encryptFilePath, decodeFilePath);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private static native boolean fileEncrypt(String normalFilePath, String encryptFilePath);
private static native boolean fileDecode(String encryptFilePath, String decodeFilePath);
}

JNI 加密代碼實現,注意加文件讀寫權限

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const char *PASSWORD = "pw";
long getFileSize(char* filePath);
extern "C"
JNIEXPORT jboolean JNICALL
Java_cn_cfanr_ndksample_utils_FileUtils_fileEncrypt(JNIEnv *env, jclass type, jstring normalFilePath_,
jstring encryptFilePath_) {
const char *normalFilePath = env->GetStringUTFChars(normalFilePath_, 0);
const char *encryptFilePath = env->GetStringUTFChars(encryptFilePath_, 0);
int passwordLen = strlen(PASSWORD);
LOGE("要加密的文件的路徑 = %s , 加密後的文件的路徑 = %s", normalFilePath, encryptFilePath);
//讀文件指針
FILE *frp = fopen(normalFilePath, "rb");
// 寫文件指針
FILE *fwp = fopen(encryptFilePath, "wb");
if (frp == NULL) {
LOGE("文件不存在");
return JNI_FALSE;
}
if (fwp == NULL) {
LOGE("沒有寫權限");
return JNI_FALSE;
}
// 邊讀邊寫邊加密
int buffer;
int index = 0;
while ((buffer = fgetc(frp)) != EOF) {
// write
fputc(buffer ^ *(PASSWORD + (index % passwordLen)), fwp); //異或的方式加密
index++;
}
// 關閉文件流
fclose(fwp);
fclose(frp);
LOGE("文件加密成功");
env->ReleaseStringUTFChars(normalFilePath_, normalFilePath);
env->ReleaseStringUTFChars(encryptFilePath_, encryptFilePath);
return JNI_TRUE;
}

解密代碼類似。

1.3 文件分割合併

Java 代碼實現

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static boolean fileSplit() {
String splitFilePath = FILE_PATH + "image.jpg";
String suffix = ".b";
try {
return fileSplit(splitFilePath, suffix, 4);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 文件合併
*
* @return
*/
public static boolean fileMerge() {
String splitFilePath = FILE_PATH + "image.jpg";
String splitSuffix = ".b";
String mergeSuffix = ".jpeg";
try {
return fileMerge(splitFilePath, splitSuffix, mergeSuffix, 4);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 文件分割
*
* @param splitFilePath 要分割文件的路徑
* @param suffix 分割文件的擴展名
* @param fileNum 分割文件的數量
* @return
*/
private static native boolean fileSplit(String splitFilePath, String suffix, int fileNum);
/**
* 文件合併
*
* @param splitFilePath 分割文件的路徑
* @param splitSuffix 分割文件的擴展名
* @param mergeSuffix 合併文件的擴展名
* @param fileNum 分割文件的數量
* @return
*/
private static native boolean fileMerge(String splitFilePath, String splitSuffix, String mergeSuffix, int fileNum);

注意,文件的分割合併需要設置文件擴展名後分割文件數量。分割時,分兩種情況,
1)能整除的,直接平均分;
2)不能整除的,fileSize % ( n -1),前 n -1 個平均分,剩餘的留給最後一個;
合併時,需要注意的是,必須按照分割的順序合併

其餘 JNI 實現代碼略,可以到 GitHub 查看具體源碼:NdkSample/native_file_handler.cpp

2. Android 增量更新

2.1 簡介

所謂增量更新,是服務器將新舊版本的 apk 做差分處理,生成一個差分包 patch,下發到客戶端;客戶端再用 patch 包和本地的 apk 合併成新的 apk,再安裝。很顯然,這樣在一定程度上可以減少更新 apk 時消耗的流量。目前在很多應用市場也有用到這種技術。增量更新技術主要解決是安裝包文件過大的問題。

2.2 優缺點

優點:節省流量,下載 apk 時,只需要下載差分包,不用下載完整包;
缺點:

  • 客戶端和服務端都需要加入相關的支持。每次新版本發佈,服務器需要根據新版本對以前所有老版本生成對應的差分包,而且還要維護不同渠道的包;另外客戶端請求時,上傳當前版本號,服務器返回對應的差分包和新版本 apk的 md5 值,作爲合併新 apk 後的校驗;所以整體流程會有點繁瑣;
  • 合成差分包會有點耗時(最好用單獨線程處理)和耗內存的,內存不足的手機或本地 apk 損壞的 apk 無法進行增量更新;另外 apk 包之間差異比較小(2m 以下)時,生成的差分包仍然有幾百 k;

2.3 差分包的生成與合併

需要用工具對文件進行 diff 和 patch 處理,一般可以通過 bsdiff實現

具體使用可以參考 Hongyang 的文章 Android 增量更新完全解析 是增量不是熱修復 - Hongyang,在這裏就不囉嗦了

注意,在執行 make 命令,可能報以下錯誤,(以下環境都是在 Mac 上)

1
2
3
4
bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?
static off_t offtin(u_char *buf)
^~~~~~
char

可以通過在bspatch.c 文件加上#include <sys/types.h>頭文件(Hongyang 沒說明清楚),參考:編譯和使用bsdiff - 木頭平 - 博客園

主要掌握幾個命令:

  • 執行 make 命令,使 makefile 生成 bsdiff 和 bspatch 可執行文件;
  • 執行 ./bsdiff old.apk new.apk update.patch,生成差分包;
  • 執行 ./bspatch old.apk new2.apk update.patch,合成新 apk;
  • 執行 md5 xxx.apk 查看 apk 的 md5 值;

2.4 服務端操作

服務端需要返回一個文件和新版本 apk md5值給客戶端。

  • 用 2.3 生成的 bsdiff 可執行文件生成新版本 apk 和老版本的 apk 的差分包 update.patch;(可以編寫個腳本處理)
  • 使用 md5 命令查看新 apk 的 md5,並保存,之後需要返回給客戶端;

2.5 客戶端操作

主要實現是如何製造 bspatch 的 so 文件,由於網上詳細的步驟都比較完善了,這裏也不囉嗦了,只簡單說下需要注意的問題。

由於按照 AS 的默認的 CMake 建 NDK 的方式,C/C++ 的文件是在 cpp 目錄的,Hongyang 的是在 jni 目錄,兩者配置方式不太一樣,如果同時使用,只會編譯 CMake 的配置,所以需要將 bspatch.c放到 cpp 目錄,同時還需要下載 bzip 源碼

詳細步驟可以參考,Android增量更新與CMake構建工具 - 亞特蘭蒂斯 - CSDN博客

PS:這裏的 CMakeLists.txt 是放在 cpp 目錄下,特別需要注意的是,由於 CMakeLists.txt 目錄改變了,必須修改 C/C++ 源文件的路徑,去掉 src/main/cpp,同時也要修改build.gradle的 cmake 文件的路徑,不然會編譯失敗

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
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
#支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall -DGLM_FORCE_SIZE_T_LENGTH")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGLM_FORCE_RADIANS")
#設置生成的so動態庫最後輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
#添加bzip2目錄,爲構建添加一個子路徑
set(bzip2_src_DIR ${CMAKE_SOURCE_DIR})
add_subdirectory(${bzip2_src_DIR}/bzip2)
add_library( native-lib
SHARED
# Provides a relative path to your source file(s). 注意,CMakeLists.txt 在 cpp 目錄下,此處不需要加路徑前綴 src/main/cpp
native_file_handler.cpp
bspatch.c
)
find_library(log-lib log )
target_link_libraries(native-lib ${log-lib} )

build.gradle 文件

1
2
3
4
5
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}

另外,修改的 bspatch.c 文件增加的 JNI 代碼中,第一個參數是 so 庫的名字,注意一定要保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//……
JNIEXPORT jint JNICALL Java_cn_cfanr_ndksample_utils_BsPatch_bspatch(JNIEnv *env, jclass jcls, jstring oldApk_,jstring newApk_, jstring patch_) {
const char *oldApkPath = (*env)->GetStringUTFChars(env, oldApk_, 0);
const char *newApkPath = (*env)->GetStringUTFChars(env, newApk_, 0);
const char *patchPath = (*env)->GetStringUTFChars(env, patch_, 0);
int argc = 4;
char* argv[argc];
argv[0] = "native-lib"; //注意此處是 so 庫名字
argv[1] = oldApkPath;
argv[2] = newApkPath;
argv[3] = patchPath;
jint ret = patchMethod(argc, argv);
(*env)->ReleaseStringUTFChars(env, oldApk_, oldApkPath);
(*env)->ReleaseStringUTFChars(env, newApk_, newApkPath);
(*env)->ReleaseStringUTFChars(env, patch_, patchPath);
return ret;
}
//……

其他代碼邏輯:

  • 1)從服務器下載差分包 update.patch 保存到本地,並請求獲取新版 apk 的 md5值;
  • 2)提取本地的 apk 文件;
  • 3)使用 JNI 方法public static native int bspatch(String oldApk, String newApk, String patch)將 update.patch 和本地舊的 apk 合併成新的 apk;
  • 4)校驗生成的新 apk 的 md 值是否和服務器返回的一樣;
  • 5)檢測新 apk 和服務器提供的一致後,安裝新的 apk 文件

不過 demo 是沒有寫從服務器下載差分包的邏輯的,這裏是將差分包通過 adb push patch路徑 /sdcard/NdkSample 命令放到手機來測試的

具體代碼可以查看 Github:NdkSample/PatchUpdateActivity.java

3. Android 封裝 libjpeg 庫

3.1 編譯 libjpeg.so 庫

  • 1.克隆 libjpeg-trubo Android 版到本地,並解壓

    1
    git clone git://git.linaro.org/people/tomgall/libjpeg-turbo/libjpeg-turbo.git -b linaro-android
  • 2.在配置好 ndk-build 環境後(具體步驟略),開始編譯 libjpeg-trubo 庫
    按照網上大多數教程的步驟都是執行以下命令

    1
    ndk-build APP_ABI=armeabi-v7a,armeabi

但可能由於我本地配置的版本是 ndk-14的,直接執行這個命令並沒有奏效,以下是我遇到的一些錯誤:

1) 如果你沒進入 libjpeg-turbo 目錄就執行命令,可能會報以下錯誤,也就是找不到 Android.mk

1
2
Android NDK: Your APP_BUILD_SCRIPT points to an unknown file: ./Android.mk
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/add-application.mk:198: *** Android NDK: Aborting... . Stop.

2) 如果報找不到應用項目的目錄,如下:

1
2
3
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-local.mk:151: *** Android NDK: Aborting . Stop.

就需要設置下 NDK_PROJECT_PATH 指定需要編譯的代碼的工程目錄,這裏給出的是當前目錄,還有,APP_BUILD_SCRIPT是Android makefile文件的路徑,如果你還有 Application.mk 文件的話,則可以添加 NDK_APP_APPLICATION_MK=./Application.mk,參考:Android開發實踐:在任意目錄執行NDK編譯 - Jhuster的專欄

3) 如果 NDK 版本過高,可能會報以下錯誤,

1
2
3
4
5
Android.mk:11: Extraneous text after `ifeq' directive
Android NDK: WARNING: Unsupported source file extensions in Android.mk for module jpeg
Android NDK: turbojpeg-mapfile
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-binary.mk:687: Android NDK: Module jpeg depends on undefined modules: cutils
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-binary.mk:700: *** Android NDK: Aborting (set APP_ALLOW_MISSING_DEPS=true to allow missing dependencies) . Stop.

所以,我最終的解決方法是用指定的低版本的 ndk (ndk-r11c)去編譯,而不是用我在系統配置 ndk。正確的命令,使用指定NDK版本編譯
~/NDK/android-ndk-r11c/ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_ABI=armeabi-v7a,armeabi

檢測已編譯成功:編譯成功後,會在 libjpeg-turbo 生成 libs 和 obj 文件夾,裏面分別會有你設置的 ABI 類型的 libjpeg.so 庫和其他生成的文件,需要拷貝到項目中的是 libs 文件下的 libjpeg.so 庫。

3.2 使用 libjpeg.so 庫編寫壓縮圖片的 native 方法

參考:Android使用libjpeg實現圖片壓縮 - BlueBerry的專欄 - CSDN博客

  • 1.拷貝 libjpeg.so 和頭文件到項目中
    首先將不同 ABI 的 libjpeg.so 拷貝到項目的 libs 目錄下,再將上面下載的 libjpeg-turbo 的源碼的所有頭文件拷貝到 cpp/include 目錄下

  • 2.配置CMakeLists文件
    練習時,要注意 CMakeLists 的配置,不然可能會發生以下錯誤(博主就是因爲沒看清楚文章,沒配置好,出錯後,一直搜索,浪費不少時間

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章