編譯工具 CMake
在Android Studio 2.2 之后,工具中增加了 CMake 的支持,于是我們有兩種選擇來(lái)編譯 c/c++ 代碼。一個(gè)是 ndk-build + Android.mk + Application.mk 組合,另一個(gè)是 CMake + CMakeLists.txt 組合。這2個(gè)組合與 Android 代碼和 c/c++ 代碼無(wú)關(guān),只是不同的構(gòu)建腳本和構(gòu)建命令。
環(huán)境配置
Android Studio 的 SDK Tools 安裝
- CMake
- LLDB
- NDK
Hello World
先新建一個(gè)項(xiàng)目,記得要勾選 C++ support,看一下 Android Studio 自動(dòng)生成的使用了 JNI 的項(xiàng)目是什么樣子的。

可以看到,與普通 Android 項(xiàng)目不同的是,支持 C++ 的項(xiàng)目在 app 目錄下多了一個(gè) .externalNativeBuild 編譯目錄與 CMakeLists.txt,main 目錄下多了 cpp 目錄。
CMakeLists 文件
關(guān)于 CMakeLists 文件的作用,我的理解是它指定了編譯 c++ 庫(kù)時(shí)所用到的一些配置,先來(lái)看看項(xiàng)目里 CMakeList.txt 文件:
// 去掉注釋
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp )
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib} )
- cmake_minimum_required(VERSION 3.4.1)
允許構(gòu)建的最低版本 - add_library(name path)
生成鏈接庫(kù),SHARED 表示生成動(dòng)態(tài)庫(kù), STATIC表示生成靜態(tài)庫(kù)。并指定了參與編譯的文件路徑 - find_library(log-lib log)
添加在編譯本地文件時(shí)依賴的庫(kù)(log),并指定別名(log-lib) - target_link_libraries(lib1 lib2 ...)
鏈接庫(kù),這里鏈接了我們自己的庫(kù) native-lib 與 log 庫(kù)
默認(rèn)的 so 庫(kù)輸出目錄為 app/build/intermediates/cmake/debug/obj/${abi} 下,可以在 CMakeLists 中指定輸出目錄
#設(shè)置生成的so動(dòng)態(tài)庫(kù)最后輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
再來(lái)看 cpp 目錄下的 native-lib.cpp 文件:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring
JNICALL
Java_com_yazhidev_cmakedemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
第一次看到 .cpp 格式的源文件肯定有點(diǎn)摸不著頭腦,Java 與 C++ 是如何通信的呢?答案就是 JNI。
JNI 規(guī)范
JNI (Java Native Interface,Java本地接口)是一種編程框架,使得 Java 虛擬機(jī)中的 Java 程序可以調(diào)用本地應(yīng)用/或庫(kù),也可以被其他程序調(diào)用。
從上面的 native-lib.cpp 文件我們可以一窺 JNI 中 C/C++ 的使用規(guī)范:
#include
C 語(yǔ)音中使用#include <>直接到系統(tǒng)指定的目錄下查找文件,我將其理解為類(lèi)似 Java 中的導(dǎo)包。JNI 中首先頭部需要引入<jni.h>,由于使用到了字符串,還導(dǎo)入了<string>。JNIEXPORT
JNIEXPORT 和 JNICALL 都是 JNI 的關(guān)鍵字,表示此函數(shù)是要被 JNI 調(diào)用的。jstring
是 JNI 中作為中介使 JAVA 的 String 與 C/C++ 的 String 交互的數(shù)據(jù)類(lèi)型,JNI的數(shù)據(jù)類(lèi)型包含兩種,分別是基本類(lèi)型和引用類(lèi)型。jobject
指代調(diào)用該方法的對(duì)象。如果 Java 中該 native 方法是靜態(tài)的,則指代該類(lèi),即 XXX.class。JNIEnv
這個(gè)env可以看做是 JNI 接口本身的一個(gè)對(duì)象,在頭部引入的 jni.h 頭文件中存在著大量被封裝好的函數(shù),這些函數(shù)也是 JNI 編程中經(jīng)常被使用到的,要想調(diào)用這些函數(shù)就需要使用JNIEnv這個(gè)對(duì)象。除了上面使用到的傳遞返回值給 Java,還有獲取類(lèi)的 class 類(lèi)型:evn->GetObjectClass(),改變 Java 中對(duì)象的某個(gè)變量的值evn-> SetIntField(...)等方法。命名方式
Java_com_yazhidev_cmakedemo_MainActivity_stringFromJNI(),JNI 中對(duì)命名有規(guī)定,命名規(guī)范為:Java_包名_class_函數(shù)名,包名中的.也要改為_,對(duì)應(yīng)的,在 Java 中引用 Native 函數(shù)也需要聲明關(guān)鍵字 native。std::string、NewStringUTF
這是 C++ 中字符串的一些寫(xiě)法,需要用時(shí)去翻一下語(yǔ)法,不多延伸。
以上只提到了項(xiàng)目里使用到的一些規(guī)范和注意點(diǎn)。下面通過(guò)寫(xiě)個(gè) demo 實(shí)際操作一下。
實(shí)戰(zhàn)
通過(guò) JNI 對(duì)圖片做變色處理。
jnigraphics 庫(kù)
這里要使用到 NDK 里提供的 jnigraphics 庫(kù),該庫(kù)
提供了基于 C/C++ 的接口,可以訪問(wèn) Android 中的 Bitmap 的像素緩沖區(qū)(bitmap buffers)。
頭文件中引入 android/bitmap.h,其典型用法如下(摘至 android/bitmap.h 詳解):
a) 用 AndroidBitmap_getInfo() 函數(shù)從位圖句柄(從JNI得到)獲得信息(寬度、高度、像素格式)
b) 用 AndroidBitmap_lockPixels() 對(duì)像素緩存上鎖,即獲得該緩存的指針。
c) 用C/C++ 對(duì)這個(gè)緩沖區(qū)進(jìn)行讀寫(xiě)
d) 用 AndroidBitmap_unlockPixels() 解鎖
我們利用該用法對(duì) bitmap 做處理。
新建 Module
首先新建個(gè) module,并新建類(lèi) BitmapUtil:
static {
// 不要忘記加載庫(kù)
System.loadLibrary("bitmap-util");
}
public class BitmapUtil {
public static native void processBitmap(Bitmap bitmap;
}
并在 main 目錄下新建 cpp 目錄,新建類(lèi) bitmap-util.cpp:
#include <jni.h>
#include <android/bitmap.h>
extern "C"
JNIEXPORT void JNICALL
Java_com_yazhidev_ndkdemo_BitmapUtil_processBitmap(JNIEnv *env, jobject /* this */, jobject bitmap) {
//構(gòu)造 AndroidBitmapInfo
AndroidBitmapInfo info = {0};
//將 bitmp 的信息填充給 info
AndroidBitmap_getInfo(env, bitmap, &info);
int *buf=NULL;
//對(duì) bitmap 解碼并獲取解碼后的像素保存在內(nèi)存中的地址指針,賦值給 srcBuf
AndroidBitmap_lockPixels(env, bitmap, (void **) &buf);
//處理像素
int w = info.width;
int h = info.height;
int32_t *srcPixs = (int32_t *) buf;
int alpha = 0xFF << 24;
int i, j;
int color;
int red;
int green;
int blue;
for (i = 0; i < h; i++) {
for (j = 0; j < w; j++) {
// get the color of per pixel
color = srcPixs[w * i + j];
red = ((color & 0x00FF0000) >> 16);
green = ((color & 0x0000FF00) >> 8);
blue = color & 0x000000FF;
color = (red + green + blue) / 3;
color = alpha | (color << 16) | (color << 8) | color;
srcPixs[w * i + j] = color;
}
}
//釋放鎖定,顯示出被修改的像素?cái)?shù)據(jù)
AndroidBitmap_unlockPixels(env, bitmap);
}
module 根目錄下新建 CMakeLists.txt 文件:
cmake_minimum_required(VERSION 3.4.1)
add_library(bitmap-util SHARED src/main/cpp/bitmap-util.cpp )
# 鏈接 jnigraphics 庫(kù)
target_link_libraries(native-lib jnigraphics)
在 module 的 build.gradle 中引用 CMakeLists 文件:
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
Java 中調(diào)用:
// Kotlin image -> ImageView(android:id="@+id/image")
val drawable = resources.getDrawable(R.mipmap.google) as BitmapDrawable
val bitmap = drawable.bitmap
BitmapUtil.processBitmap(bitmap)
image.setImageBitmap(bitmap)
遇到的問(wèn)題
java.lang.UnsatisfiedLinkError: No implementation found
extern "C"
擴(kuò)展
項(xiàng)目中導(dǎo)入 so 庫(kù)
在使用 JNI 時(shí)有時(shí)可能只有編譯好的 so 庫(kù),那么如何在項(xiàng)目中使用 so 庫(kù)呢?
右鍵 app 目錄,選擇 new - Folder -JNI Folder,新建一個(gè) JNI 目錄用于存放 so 文件。
so 庫(kù)(CPU)的兼容
使用 CMake 編譯 so 庫(kù)時(shí),可通過(guò)配置 gradle 文件指定編譯的 so 庫(kù)架構(gòu)
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
// 生成.so庫(kù)的目標(biāo)平臺(tái)
abiFilters "armeabi-v7a", "armeabi", "x86"
}
}
}
}
對(duì)于CPU來(lái)說(shuō),不同的架構(gòu)并不意味著一定互不兼容,根據(jù)目前Android共支持七種不同類(lèi)型的CPU架構(gòu),其兼容特點(diǎn)可總結(jié)如下:
armeabi設(shè)備只兼容armeabi;
armeabi-v7a設(shè)備兼容armeabi-v7a、armeabi;
arm64-v8a設(shè)備兼容arm64-v8a、armeabi-v7a、armeabi;
X86設(shè)備兼容X86、armeabi;
X86_64設(shè)備兼容X86_64、X86、armeabi;
mips64設(shè)備兼容mips64、mips;
mips只兼容mips;
根據(jù)以上的兼容總結(jié),我們還可以得到一些規(guī)律:
armeabi的SO文件基本上可以說(shuō)是萬(wàn)金油,它能運(yùn)行在除了mips和mips64的設(shè)備上,但在非armeabi設(shè)備上運(yùn)行性能還是有所損耗;
64位的CPU架構(gòu)總能向下兼容其對(duì)應(yīng)的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips。
更多 so 文件的信息可參考:Android SO文件的兼容和適配
參考
AndroidStudio項(xiàng)目CMakeLists解析
JNI技術(shù)規(guī)范
Android NDK之旅——圖片高斯模糊
Jni接口-深入研究參數(shù)的傳遞(一)
android/bitmap.h 詳解