2019年4月2日 星期二

Android 透過 jni 呼叫C

稍微說個為何要在安卓使用C語言開發東西,大學專題有用到手機寫影像辨識,利用camera2 api擷取到影像,直接用android sdk的bitmap + get/set pixel 硬幹,後果當然是慘不忍睹,效能說能多慢就有多慢,好像一張640*480的影像轉成灰階就要幾分鐘,而那時候採取的臨時解決方案是用get/set pixels ,這個也是sdk提供的原始方法之一,是利用陣列先做完運算再一次處理,減少了反覆呼叫方法的次數,結果速度有了很大的提升,就一直用這個方法到畢業展了,但我知道如果做影像處理,遲早還是要和底層打交道,而唸研究所的時間,打算來面對。

日前在github上有看到一個照片濾鏡的專案,核心照片濾鏡處理是一個library,透過JNI call底層C語言來加速,我覺得很重要,所以這篇分成兩個方式實作Android呼叫jni (javah、CMake)來測試看看

濾鏡專案 : https://github.com/Zomato/AndroidPhotoFilters





第一種 : 利用javah、ndkbuild


1. AS裡面 -> SDK Manager -> SDK Tools -> 先把CMake、NDK 裝起來,沒問題的話IDE會自動設置套件路徑




2. AS裡面 -> File -> Settings -> Tools -> External Tools



3. 上面 +號 新增Tools (這個動作主要是把指令和參數寫成工具,以後要使用可以直接套用)




4. 新增 javah


參數 :

C:\Program Files\Java\jdk1.8.0_201\bin\javah.exe (這裡要對應自己jdk的路徑 需要注意的是用jdk10以下的,以上的沒有javah)

-v -jni -d $ModuleFileDir$/src/main/jni $FileClass$

$SourcepathEntry$




5. 新增 ndkbuild


參數 :

D:\Android\Sdk\ndk-bundle\ndk-build.cmd (這裡要對應自己ndk的路徑)

NDK_PROJECT_PATH=$ModuleFileDir$/build/intermediates/ndk NDK_LIBS_OUT=$ModuleFileDir$/src/main/jniLibs
NDK_APPLICATION_MK=$ModuleFileDir$/src/main/jni/Application.mk APP_BUILD_SCRIPT=$ModuleFileDir$/src/main/jni/Android.mk V=1

$ProjectFileDir$



6. 在Main方法下新增一個java類別 myNDK , 裡面寫兩個對應c語言要呼叫的function


public class myNDK {
    static {
        System.loadLibrary("myJNI");
    }
    public native  String getMycstring();
    public native int getJniAdd(int a, int b);
}




7. 切成Project模式 -> 在src/main下面新增 jni 資料夾


8. 設定app的build.gradle (這裡的modeulName 要和在 myNDK loadLibrary寫的一樣,jniLibs是之後ndkbuild放置so檔的地方)


9. 對剛剛新增的myNDK按右鍵 -> External Tools -> javah ,就會發現指令會自動執行,jni目錄下會產生header檔
裡面分別有兩個對應myNDK的function




10. 接著就是在jni目錄下,新增c++檔案 ,檔名必須和myNDK裡面loadLibrary那個名字一樣,所以取作myJNI.cpp

*注意,這裡的function要根據header的方法參數寫,不然會找不到拋錯


#include "com_aaron_jnitester3_myNDK.h"

JNIEXPORT jstring JNICALL Java_com_aaron_jnitester3_myNDK_getMycstring
        (JNIEnv *env, jobject){
    return (*env).NewStringUTF("MY !!  NDKString!!");
}

JNIEXPORT jint JNICALL Java_com_aaron_jnitester3_myNDK_getJniAdd
        (JNIEnv *env, jobject object, jint a, jint b) {
    return a + b;
}



11. 接著就是在jni目錄下新增File Android.mk 和 Application.mk

Android.mk


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := myJNI
LOCAL_SRC_FILES := myJNI.cpp

include $(BUILD_SHARED_LIBRARY)



Application.mk


APP_ABI := all



12. 對jni目錄按右鍵 -> External Tools -> ndkBuild , 會發現指令自動執行,並且新增一個叫jniLibs的資料夾,裡面放置手機版本的so檔


13. 撰寫demo程式


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.textView);

        myNDK myndk = new myNDK();
        textView.setText(myndk.getMycstring() + myndk.getJniAdd(8,7));  //呼叫NDK方法
  
    }
}



DEMO :





Source Code : https://github.com/tks3589/BloggerCode/tree/master/android/jniTester3






第二種 : 利用CMake,直接建立預設支援C++專案


1. 直接 File -> New -> New Project -> Native C++



2. 裡面的檔案結構長這樣,基本上要寫自己的function,只要在Main裡面直接加,Alt + Enter 就會自動幫你在cpp檔裡面補方法了






DEMO:




CMakeLists.txt裡面應該是更改library的參數,沒用過先記錄


build出來的模擬器apk有 so檔案


Source Code : https://github.com/tks3589/BloggerCode/tree/master/android/jniTester4





結論 :

第二種方法明顯快速許多,但陽春點的做法能更理解原理也是好事 ,還不是很熟悉,有問題的地方還請大家多多提出,謝謝。















沒有留言:

張貼留言