關於在Android中使用CMake你所需要了解的一切(一)

xong發表於2018-09-30

​ 相信大家在開發的過程中,都或多或少的接觸過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專案

​ 這個就很簡單啦,新建專案的時候把 include C++ support 勾選上就可以了,但,我們還是自己動手來一遍,不依靠IDE,看看可不可以,新建普通專案,名字為:AndCmake,新建完成後如下:

關於在Android中使用CMake你所需要了解的一切(一)

新專案,什麼都沒有,我們都知道用CMake的話,必須要有CMakeLists.txt(注意名字不能寫錯),第二個就是需要的cpp資源啦,這就很簡單了,在src/main目錄下新建就好了,為了整潔在main目錄下新建cpp資料夾,且在裡面新建CMakeLists.txt和native_hello.cpp檔案,然後在去CMakeLists.txt簡單的配置下就好了,完成後,如下:

  1. ../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"也不能算錯,也是對的。

  2. ../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++部分寫好了

  3. 修改../app/build.gradle,修改後如下:

    android {
        ...
        defaultConfig {
            ...
            externalNativeBuild {
                cmake {
                    arguments '-DANDROID_STL=c++_static'
                }
            }
            ...
        }
        ...
        externalNativeBuild {
            cmake {
                path 'src/main/cpp/CMakeLists.txt'
            }
        }
        ...
    }
    複製程式碼
  4. 寫對應的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();
    }
    
    複製程式碼
  5. 呼叫NativeFun.stringFromJNI(),檢視是否有正確的資訊輸出:這就很簡單啦,在Activity中新增一個TextView然後顯示下就好了,正確的話,應該會有如下顯示:

    關於在Android中使用CMake你所需要了解的一切(一)

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());
}
複製程式碼

對於我們來講:

  1. 儘量不寫重複程式碼
  2. 程式碼要高效

對於上面的函式名來講,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 目錄如下:

關於在Android中使用CMake你所需要了解的一切(一)

在預設的情況下會生成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下專案,如下圖:

關於在Android中使用CMake你所需要了解的一切(一)

這樣打入apk中的就只有v7a和x86兩種架構的so庫啦! 下篇我們來講解一下 現有的cpp程式碼整合到專案中有幾種方式,以及如何操作。

點選跳轉到下一篇:如何將現有的cpp程式碼整合到專案中

Demo連結:UseCmakeBuildLib


END

相關文章