相信大家在開發的過程中,都或多或少的接觸過JNI,然後每次要接觸JNI的時候,倒吸一口冷氣,太難啦!
只有Java程式碼和C++程式碼 還好,在新建專案的時候把那個 "Include C++ support"勾選上,然後一路next,最後finish,一個簡單的帶有C++程式碼的Android專案就算完成了,然後在看下CMakeLists.txt怎麼寫的,照貓畫虎就可以了,但是實際開發中,並不簡單,因為底層(C++)那裡給過來的有可能是.a靜態庫,也有可能是.so動態庫,然後如何整合進專案裡就是:一臉懵逼,二臉茫然,三臉不知所措。由於客觀事實只能去百度,然後百度到的全是用Android.mk實現的居多,然而現在都Android Studio 3.1+時代了,還mk?關於CMake的資料又很少,所以就有了本文。
在去年在公司還是實習的時候,老大丟給我一個.a靜態庫(當時並不知道這是一個靜態庫),很是好奇,很想知道.a是怎麼構建出來的,a和so的區別又是什麼,總之一大堆疑問,在轉正後,也嘗試過要構建一個.a靜態庫,無奈!百度到的都是用mk的咯,找到一篇CMake的,但竟然要去Linux下面編譯,還要什麼自定義工具鏈,總之,麻煩的一批。
所以就有了這一系列,本系列不闡述什麼是CMake,什麼是NDK,什麼是JNI,只是寫一下在Android中使用CMake常遇的問題,解決方法是什麼。
本系列涉及到的有:
- 初次使用CMake構建native專案
- 如何將現有的cpp程式碼整合到專案中
- 拷貝原始碼
- 編譯成庫檔案
- CMake連結a靜態庫以及so動態庫及動態庫和靜態庫的區別
初次使用CMake構建native專案
這個就很簡單啦,新建專案的時候把 include C++ support 勾選上就可以了,但,我們還是自己動手來一遍,不依靠IDE,看看可不可以,新建普通專案,名字為:AndCmake,新建完成後如下:
新專案,什麼都沒有,我們都知道用CMake的話,必須要有CMakeLists.txt(注意名字不能寫錯),第二個就是需要的cpp資源啦,這就很簡單了,在src/main目錄下新建就好了,為了整潔在main目錄下新建cpp資料夾,且在裡面新建CMakeLists.txt和native_hello.cpp檔案,然後在去CMakeLists.txt簡單的配置下就好了,完成後,如下:
-
../src/main/cpp/native_hello.cpp:
// // Created by xong on 2018/9/28. // #include<jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz) { std::string hello = "Hello,I from C++"; return env->NewStringUTF(hello.c_str()); } 複製程式碼
這裡需要說明以下,Java_com_xong_andcmake_jni_是去java的一層一層的包下面去尋找,比如這個就是去com.xong.andcmake.jni下面找,後面的NativeFun_stringFromJNI就是類和方法名了。函式的形參兩個必須是這樣的,第二個形參可以寫成"jobject",我最近寫JNI的時候,提示改成"jclass"但是,"jobject"也不能算錯,也是對的。
-
../src/main/cpp/CMakeLists.txt
# CMake最低版本 cmake_minimum_required(VERSION 3.4.1) # 將需要打包的資源新增進來 add_library( # 庫名字 native_hello # 庫型別 SHARED # 包含的cpp native_hello.cpp ) # 連結到專案中 target_link_libraries( native_hello android log ) 複製程式碼
這就把C++部分寫好了
-
修改../app/build.gradle,修改後如下:
android { ... defaultConfig { ... externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_static' } } ... } ... externalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } } ... } 複製程式碼
-
寫對應的Java層程式碼,在com.xong.andcmake包下新建jni,然後新建NativeFun類,程式碼如下:
package com.xong.andcmake.jni; /** * Create by xong on 2018/9/28 */ public class NativeFun { static { System.loadLibrary("native_hello"); } public static native String stringFromJNI(); } 複製程式碼
-
呼叫NativeFun.stringFromJNI(),檢視是否有正確的資訊輸出:這就很簡單啦,在Activity中新增一個TextView然後顯示下就好了,正確的話,應該會有如下顯示:
OK,這樣我們的第一個帶有CPP程式碼的APP就算完成了,然後我們來思考一下,先來看native_hello.cpp的程式碼:
//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#include <string>
extern "C" JNIEXPORT
jstring JNICALL
// 這裡可不可以優化一下?
Java_com_xong_andcmake_jni_NativeFun_stringFromJNI(JNIEnv *env, jclass thiz)
{
std::string hello = "Hello,I from C++";
// 這裡呢?
return env->NewStringUTF(hello.c_str());
}
複製程式碼
對於我們來講:
- 儘量不寫重複程式碼
- 程式碼要高效
對於上面的函式名來講,Java_com_xong_andcmake_jni_,假如我們在Java中對應的native類和方法,感覺在這個包中不合適,要換下包名,但是我這個類中有很多native方法,只要一換包,那麼對應的cpp中的函式名就要挨個改,函式少還好辦,假如有1000個,改起來真的是不要太爽,而且容易丟,那麼有沒有辦法來儘量的減少工作量呢?類和包是相對固定的,那麼我們能不能把包名抽取出來用一個表示式來表示呢?沒錯!就是巨集定義啦!
對於下面返回的那一句,我為啥要這樣寫呢,因為用Android Studio建立一個帶有cpp專案的時候,裡面就是這樣寫的,然後就很自然的抄來了,但是我們想一下,有這樣寫的必要嗎?可以看到的是在最後返回的時候,string物件又經過了一層轉換點進去後可以發現,是將 string物件又轉成了char陣列(當然在C++中是char*指標還是const的,這裡不需要知道const是啥,只需要知道,c_str()其實是又將string物件轉成了char[]陣列),然後我們這樣寫是不是就…多此一舉,讓計算機多做了一些事情呢?所以改完後的程式碼如下:
//
// Created by xong on 2018/9/28.
//
#include<jni.h>
#define XONGFUNC(name)Java_com_xong_andcmake_jni_##name
extern "C" JNIEXPORT
jstring JNICALL
XONGFUNC(NativeFun_stringFromJNI)(JNIEnv *env, jclass thiz)
{
return env->NewStringUTF("Hello,I from C++");
}
複製程式碼
這樣的話,假如NativeFun不在jni包下了,而是在jnia包下,那麼我們只需要將上面的巨集定義改下就好了;
因為我們沒有對string做操作,那麼我們就不需要它啦,直接返回就好了,這樣計算機就不那麼累了。
make project後,發生了什麼?
接觸過JNI的同學應該瞭解,專案裡面新增了cpp程式碼,apk會變的很大,那麼原因嘞,那麼我就來捋一捋,cpp的程式碼再make project後都發生了什麼,都很清楚會生成so,那麼…在預設的情況下會生成幾個?
開啟 ../app/build/intermediates/cmake/debug/obj 目錄如下:
在預設的情況下會生成4個,v7a的是最大的,x86_64是最小的,預設情況下,這4個是都會打到apk裡面的(因為不清楚到底是往那臺手機上安裝呀,只能將所有的資源都打到apk裡面了),要想縮小apk,那麼就把需要的so打入apk就好了。然後,問題來了,怎麼才能生成規定的so庫呢?上程式碼,如下:
apply plugin: 'com.android.application'
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
}
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
...
}
...
}
...
複製程式碼
在../app/build.gradle中新增ndk標籤,且在裡面寫上需要生成的架構名稱就可以了,重新build下專案,如下圖:
這樣打入apk中的就只有v7a和x86兩種架構的so庫啦! 下篇我們來講解一下 現有的cpp程式碼整合到專案中有幾種方式,以及如何操作。
點選跳轉到下一篇:如何將現有的cpp程式碼整合到專案中
Demo連結:UseCmakeBuildLib
END