終於找到一篇極佳的 NDK 入門文章

  • 前言
  • 筆者看了一些NDK的項目。一些教程不是HelloWord就是直接整FFmpeg或OpenCV,可謂一個天一個地,而且目錄結構和Android3.5的默認結構並不是太一致,一直沒找到什麼合心的文章。故寫此文連接這天地,來總結一下在NDK開發之前你應知道的東西。


    • 在此之前,先劃分三類人,如果不認清自己是什麼角色(垃圾)就去玩NDK,你會很糟心:

    user : 純粹.so鏈接庫使用者(伸手黨)creator : 純粹ndk開發者,創作.so鏈接庫(創作家)designer : 在現有的.so上自己開發.so鏈接庫實現特定功能(程序設計師)


    • 本文內容
    <code>1.本文將以user、creator、designer三者的視角來看NDK
    2.AndroidStdio3.5的默認目錄結構
    3.有現成的C++代碼,如何讓Android調用它?
    4.arm64-v8a、armeabi-v7a、x86、x86_64分別是幹嘛的?
    5.動態鏈接庫.so是什麼鬼,如何從c/c++生成.so?
    6.libs,jniLibs,jin目錄到底該怎麼放?如何自定義文件放置的位置?
    7.一些讓人糟心的異常
    /<code>

    • 前置知識

    也許你很怕C++,就像你在新手村被3級的boss虐到心理陰影,但是你現在已經50級了,還怕曾經虐你的3級的boss嗎? 建議閱讀: [- C++趣玩篇1 -] 從打印開始說起 ,這篇對本文很重要, 是簡單,也很有趣。現在情況如此:上篇中C++實現了一個打印臉的類,我想在Android中使用它。

    終於找到一篇極佳的 NDK 入門文章


    一、對於純粹.so使用者(User)

    1.目錄結構

    當你只是單純的使用動態鏈接庫.so中的已有功能,也就是傳說中的伸手黨。那你與NDK只是擦肩而過,並不需要理會C/C++,也不需要創建一個NDK的項目,甚至連JNI都有現成的。你所需要做的只是在main下新建jniLibs,經過測試,其為默認的.so成放置地,此時gradle文件你可以一字不動。

    終於找到一篇極佳的 NDK 入門文章


    2.JNI接口定義

    俗話說拿人家手短,吃人家嘴軟。由於JNI是根據包名找到C/C++函數的,使用時必須和creator定義的接口完全一致(包括包名)。

    <code>---->[com.toly1994.jni_creator.Facer]--by 張風捷特烈-----
    package com.toly1994.jni_creator;
    public class Facer {
    public static native String getFacer( String top, String bottom, String brow, String eyes);
    }
    /<code>

    3.庫的使用

    這個庫是等會要創造的,這裡先來演示。System.loadLibrary指定庫名其中庫全名為libtoly_facer-lib.so,加載時toly_facer-lib即可這樣在上一篇[- C++趣玩篇1 -] 從打印開始說起中實現的打印類就可以在Android中使用。


    <code>public class MainActivity extends AppCompatActivity {
    static {//加載類庫
    System.loadLibrary("toly_facer-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    TextView textView=findViewById(R.id.hello);
    textView.setTextSize(30);
    //通過native接口getFacer使用類庫中C++方法
    textView.setText(Facer.getFacer("-", "-", "~", "X"));
    }

    }
    /<code>

    OK,現在80%的人問題解決了。(手動搞笑)


    二、對於純粹ndk開發者(Creator)

    如果你有現成的C++代碼想要直接用在Android上,或者想要手擼個什麼高效的框架,或者想要讓你的源碼不容易破解,那麼廢話不多說,就開整吧。哥敬你是條好漢。現在你需要創建一個Native C++ 的Android項目。這裡就來實現toly_facer-lib

    1.準備活動

    上一篇中已經完成了C++類

    • 頭文件
    <code>--->[app/src/main/cpp/Facer.h]----
    //
    // Created by 張風捷特烈 on 2019/10/3.
    //

    #include <iostream>

    using namespace std;

    #ifndef TOLYC_FACER_H
    #define TOLYC_FACER_H

    class Facer {
    public:
    Facer(const string &top="#", const string &bottom="#", const string &brow="~", const string &eyes=".");
    ~Facer();
    public:
    string top;
    string bottom;

    string brow;
    string eyes;
    public:
    void printFace() ;
    string getFace();
    };

    #endif //TOLYC_FACER_H
    /<iostream>/<code>

    • cpp實現文件
    <code>--->[app/src/main/cpp/Facer.cpp]----
    //
    // Created by 張風捷特烈 on 2019/10/3.
    //

    #include "Facer.h"

    Facer::Facer(
    const string &top,
    const string &bottom,
    const string &brow,
    const string &eyes) : top(top),
    bottom(bottom),
    brow(brow),
    eyes(eyes) {}

    void Facer::printFace() {
    cout<< getFace() << endl;
    }

    Facer::~Facer() {

    }

    string Facer::getFace() {
    string result;
    for (int i = 0; i < 10; ++i) {
    i != 9 ? result+=top : result+=top+"\\n";
    }
    result+= "| " +brow + " " + brow + " |" +"\\n";
    result+= "| " +eyes + " " + eyes + " |" +"\\n";
    result+= "| -} |\\n";
    for (int i = 0; i < 10; ++i) {
    i != 9 ? result+=bottom : result+=bottom+"\\n";
    }

    return result;
    }
    /<code>

    2.項目結構

    新建Native C++ 的項目之後,main文件夾下會有cpp文件夾,這就是C++代碼的家如果直接將兩個Facer文件拷貝進去,會飄紅。因為還沒有在CmakeLists中進行配置

    終於找到一篇極佳的 NDK 入門文章


    3.CmakeLists中的配置
    <code>cmake_minimum_required(VERSION 3.4.1)

    add_library(toly_facer-lib SHARED
    toly_facer-lib.cpp Facer.h Facer.cpp)#直接加入文件

    find_library(log-lib log)

    target_link_libraries(toly_facer-lib ${log-lib})
    /<code>

    當然也許你肯定懶得一個個添加,可以加載cpp文件夾下的所有.cpp和.c文件

    <code>cmake_minimum_required(VERSION 3.4.1)

    #定義全局 my_source_path 變量
    file(GLOB my_source_path
    ${CMAKE_SOURCE_DIR}/*.cpp
    ${CMAKE_SOURCE_DIR}/*.c)

    add_library(toly_facer-lib
    SHARED ${my_source_path})

    find_library(log-lib log)

    target_link_libraries(toly_facer-lib ${log-lib})
    /<code>

    4.設計JNI的native接口方法和C++實現

    此方法所屬類名、包名對user都至關重要。對於creator隨意啦,就是任性

    <code>---->[src/main/java/com/toly1994/jni_creator/Facer.java]----
    package com.toly1994.jni_creator;

    public class Facer {
    public static native String getFacer( String top, String bottom, String brow, String eyes);
    }
    /<code>

    C++與Java的相互作用,就是Java進行輸入,經C++轉化將有價值的東西傳給Java端

    <code>---->[src/main/cpp/toly_facer-lib.cpp]----
    #include <jni.h>
    #include <string>
    #include "Facer.h"

    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_toly1994_jni_1creator_Facer_getFacer(JNIEnv *env, jclass clazz, jstring top,
    jstring bottom, jstring brow, jstring eyes) {
    Facer facer(//使用 env->GetStringUTFChars將jstring轉化為string
    env->GetStringUTFChars(top, 0),
    env->GetStringUTFChars(bottom, 0),
    env->GetStringUTFChars(brow, 0),
    env->GetStringUTFChars(eyes, 0)
    );

    return env->NewStringUTF(facer.getFace().c_str());
    }
    /<string>/<jni.h>/<code>

    基本上流程就是這樣。


    三、掃盲科普

    1.arm64-v8a、armeabi-v7a、x86、x86_64
    <code>arm 架構注重的是續航能力:大部分的移動設備
    x86 架構注重的是性能:大部分的臺式機和筆記本電腦

    arm64-v8a :第8代、64位ARM處理器
    armeabi-v7a :第7代及以上的 ARM處理器
    x86:x86 架構的 CPU(Intel 的 CPU)
    x86_64:x86 架構的64位 CPU(Intel 的 CPU)

    /<code>

    默認會編譯出四種.so文件

    終於找到一篇極佳的 NDK 入門文章


    2.配置輸出的.os架構類型

    可以通過app下的build.gradle來指定編譯的.so類型注意只有這四種類中,以前很多項目中存在abiFilters 'armeabi'但現在會崩

    終於找到一篇極佳的 NDK 入門文章

    <code>android {
    defaultConfig {
    externalNativeBuild {
    cmake {
    abiFilters 'armeabi-v7a', 'arm64-v8a'
    }
    }
    }
    /<code>

    這樣清一下項目,再編譯出來的只有'armeabi-v7a', 'arm64-v8a'此時運行到模擬器上,會發現找不到類庫,則說明模擬器去X86的。運行到真機無誤,則說明真機是arm的


    3..so文件是什麼?

    如果說.dll估計你會說:哦,好像見過。其實.so和.dll並沒有本質的區別,它們都是一個C++實現的功能團。只不過.so是用在linux上的,.dll是用在Windows上的。如今操作系統三足鼎立,當然少不了MacOS,類似的在MacOS中有.dylib文件。它們都是 C++ 的動態鏈接庫(Dynamic Link Library )

    而Android作為Linux的一員,C++ 編譯出的.so便是順理成章那如何將C++編譯成.so庫?這便是NDK在做的事,也是上面在做的事。打包時gradle會將對應的.so包打到apk裡,然後.so就能在linux裡愉快的玩耍了。

    終於找到一篇極佳的 NDK 入門文章


    4.如何自定義資源文件位置

    個人建議習慣優於配置,用默認挺好的。如果你是非常有個性的...也可以在gradle裡進行制定雖然你也許不會用到,但是看一下,看到要認得,不至一臉蒙圈。對於使用者,可以隨意指定盛放.so的文件夾,需要在app下的build.gradle配置

    終於找到一篇極佳的 NDK 入門文章

    <code>android {
    ...
    sourceSets {

    main {
    jniLibs.srcDirs = ['target']//指定so庫的位置,加載so庫
    }
    }

    }

    /<code>

    對於創造者,也可以使用jni.srcDirs來指定C++代碼盛放的位置

    <code>sourceSets.main{
    jni.srcDirs = ["src/main/cpp"]
    }
    /<code>

    四、對於程序設計師(Designer)

    俗話說難的不是重寫,而是對爛代碼的重構,有時候修改比創作更難已有的.so文件但功能上又需要定製,於是第三類就誕生了,也是最頭疼的其實FFmpeg和OpenCV等都是這第三類,用已存在事物去構建新事物,便是設計。

    1.項目結構

    算法和核心代碼已經實現,我們需要做的是結合業務進行接口封裝及方法調用這裡我就用OpenCV的使用來進行演示: 你需要創建的是Native C++項目(Opencv下載什麼的,不廢話了,詳見:OpenCV專題1 - AndroidStudio的JNI工程及引用OpenCV)

    終於找到一篇極佳的 NDK 入門文章


    2.你的角色

    這時,你是設計者,兼具創造者和使用者兩重角色。但比純粹的創造要簡單,比純粹的使用要難。這時可以通過CmakeLists去鏈接到OpenCV的.so文件,這樣你就可以使用OpenCV的頭文件進行功能實現

    <code>cmake_minimum_required(VERSION 3.4.1)

    include_directories(include)#引入include文件夾

    #定義全局 my_source_path 變量
    # file(GLOB my_source_path ${CMAKE_SOURCE_DIR}/*.cpp ${CMAKE_SOURCE_DIR}/*.c)
    aux_source_directory(. my_source_path) #上行的簡化:將本文件夾下文件加入
    add_library(toly_cv SHARED ${my_source_path})

    #添加動態鏈接庫
    add_library(lib_opencv SHARED IMPORTED)
    set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION
    ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libopencv_java4.so) #so文件位置

    ## 在ndk中查找log庫 取別名log-lib
    find_library(log-lib log)
    # 在ndk中查找jnigraphics庫 取別名jnigraphics-lib jnigraphics
    find_library(graphics jnigraphics)

    target_link_libraries(
    toly_cv
    lib_opencv
    jnigraphics
    log
    )
    /<code>

    你可以定義一個JNI接口來暴露你在C++層實現的方法,再打包成.so供他人使用這便是開源的魅力,比如下面的灰色圖像,使用者可以拿著打出的.so包通過TolyCV來使用


    <code>---->[com.toly1994.jni_designer.TolyCV]----
    public class TolyCV {
    public static native Bitmap grayBitmap(Bitmap bitmap,Bitmap.Config argb8888);
    }

    ---->[src/main/cpp/toly_cv.cpp]----
    #include <jni.h>
    #include <string>
    #include "bitmap_utils.h"
    #include <opencv2>

    extern "C"
    JNIEXPORT jobject JNICALL
    Java_com_toly1994_jni_1designer_TolyCV_grayBitmap(JNIEnv *env, jclass clazz, jobject bitmap,jobject argb8888) {
    Mat srcMat;
    Mat dstMat;
    bitmap2Mat(env, bitmap, &srcMat);
    cvtColor(srcMat, dstMat, CV_BGR2GRAY);//將圖片的像素信息灰度化盛放在dstMat
    return createBitmap(env,dstMat,argb8888);//使用dstMat創建一個Bitmap對象

    }
    /<opencv2>/<string>/<jni.h>/<code>

    五、讓人糟心的異常

    筆者也並非一路暢通無阻,走的坑也挺多,下面幾個坑來給你說說

    1.ninja: error: 巴拉巴拉... missing and no known rule to make it
    終於找到一篇極佳的 NDK 入門文章

    仔細排查CmakeLists,可能是.so文件的路徑不對


    2.CMake Error at 巴拉巴拉... (add_library):
    終於找到一篇極佳的 NDK 入門文章

    仔細排查CmakeLists,可能是你的C++代碼文件路徑不對


    3.java.lang.UnsatisfiedLinkError: 巴拉巴拉... "XXX.so"
    終於找到一篇極佳的 NDK 入門文章

    說明你的庫加載異常,看看你的庫名有沒有寫對


    4. java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String 巴拉巴拉...
    終於找到一篇極佳的 NDK 入門文章

    說明你的JNI接口和.so比匹配,自行匹配放到相應包名下

    待續...



    分享到:


    相關文章: