1. 程式人生 > >一篇好文之Android 呼叫C程式碼及生成除錯so庫

一篇好文之Android 呼叫C程式碼及生成除錯so庫

不靠譜的朱先生又來了,今天是週五,我又出現了!好了,不為自己多解釋,上週沒發文章,其實我寫了,只是沒有發出來而已……機智ovo。

上週寫的文章是關於GreenDao全面解析,其實當時是想寫一個關於資料庫的系列文章,後來一共就寫了兩篇,SQlite全面解析和GreenDao全面解析。至於原先計劃的編寫LitePal, Realm,wcdb的介紹文章可能要推後了,因為現在計劃是先把FFmpeg這一系列的文章給寫出來,畢竟我的主修的是音視訊開發!在學習FFmpeg的過程中,我想以這篇文章作為這個系列的第一篇!ok,讓我們一塊學習如何在Android中編輯呼叫C語言及如何編譯成so庫供其他專案呼叫,以及最後如何使用別人專案中so庫檔案?

大家在使用學習使用so庫的時候遇到什麼問題,歡迎在我的公眾號aserbao給我留言,無償服務!同時,歡迎大家來加入微信群二維碼討論群,一起討論Android開發技術!群二維碼定時在我公眾號更新!
在這裡插入圖片描述

好了,在學習這篇文章之前,請檢視下如下幾個問題(本文主要解決的問題列表):

  1. 你知道Android 如何呼叫C程式碼?
  2. 你知道如何生成so檔案?
  3. 你知道如何同時生成多個so檔案?
  4. 你知道如何將多個so檔案合併成一個so檔案?
  5. 你知道如何呼叫so庫檔案?
  6. 你知道如何檢視so中的類名,路徑和方法名?

好了,看了上面問題無論你知道幾個,我都建議你看下這篇文章,因為畢竟寫了那麼久,麻煩幫我看下有沒有寫錯的地方,最後無論好壞,幫忙點評下,最後點個贊就再好不過了!

最後,溫馨提示:未成年人(未滿18週歲),本文內容可能會引起你的不適,請在家長的陪伴下進行觀看!

文章目錄

編譯環境

  • Android Studio 3.0.1
  • macOS Mojava 10.14
  • android-ndk-r16b-darwin-x86_64.zip

如何生成so檔案?

Android 中生成so檔案的方式有三種,第一種是通過ndk來編寫so庫檔案(官方在下個Android Studio將刪除這種方式,不建議使用)。第二種是通過ndk-build來生成so檔案;第三種是通過CMake來編譯生成so檔案。我們著重講解後面兩種;別問我為什麼,腦殼疼,誰讓第一種即將被淘汰了呢!

1. 通過ndk 編譯生成so(已棄用)

通過Ndk來生成so檔案,我之前有寫過關於此種實現的文章:NDK快速整合祕籍但是自Android Studio 3.0.1之後,使用這種方式會給如下提示,建議使用Cmake或者ndk-build整合;

Error: Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio.  Please switch to a supported build system.
Consider using CMake or ndk-build integration. For more information, go to:
 https://d.android.com/r/studio-ui/add-native-code.html#ndkCompile
 To get started, you can use the sample ndk-build script the Android
 plugin generated for you at:
 /Users/aserbao/aserbao/code/code/github/functions/audioAndvideo/ffmpeg/TestNDK/app/build/intermediates/ndk/debug/Android.mk
Alternatively, you can use the experimental plugin:
 https://developer.android.com/r/tools/experimental-plugin.html
To continue using the deprecated NDK compile for another 60 days, set 
android.deprecatedNdkCompileLease=1541041326840 in gradle.properties

2. 通過ndk-build編譯生成so庫

1. ndk-build編譯單個so庫

ndk-build編寫so檔案和ndk編寫步驟上差不多,區別在於: ndk-build不需要gradle.properties檔案中新增android.useDeprecatedNdk=true, 且ndk-build需要編寫Android.mk檔案,下面是生成的步驟:

  1. 建立一個呼叫本地方法的類檔案;
    新建一個專案,在MainActivity同級目錄下建立CallUtils類;(路徑隨意,命名隨意,這一步主要是為了生成對應的.h檔案)我的CallUtils類程式碼
public class CallUtils {
    static {
        System.loadLibrary("use_ndk_build");
    }
    public static native String callSimpleInfo();
}
  1. 生成對應的.h程式碼,找到CallUtils類的路徑,輸入如下命令,生成對應的.h檔案:
 javac CallUtils.java

 javac -h . CallUtils.java// java jdk 1.8以後刪除javah命令了,所以我這裡使用javac -h替代,如果可以使用javah可以直接使用
  1. 將.h檔案重新命名use_ndk_build.c並剪貼至jni目錄下(沒有請在app目錄下新建jni),程式碼如下:
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild_CallUtils_callSimpleInfo
  (JNIEnv *env, jclass ojb){//注意這個地方宣告env和ojb,不然會報錯。
     return (*env) -> NewStringUTF(env,"Hello, I'm an info come from use ndk-build");
  };
  1. 在jni目錄下建立Android.mk檔案,並輸入:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 匯出的so庫名字
LOCAL_MODULE := use_ndk_build
# 對應的c程式碼
LOCAL_SRC_FILES := jni/use_ndk_build.c
include $(BUILD_SHARED_LIBRARY)
  1. 在jni目錄下建立Application.mk檔案,內容如下:
    生成對於架構下的so包,如果只需要生成armeabi-v7a下的so包,就只輸入APP_ABI := armeabi-v7a 就行;
APP_ABI := armeabi armeabi-v7a x86 mips arm64-v8a mips64 x86 x86_64
  1. 開啟Terminal,執行ndk-build命令
    輸入ndk-build命令後,就可以到libs目錄下檢視編譯生成的so庫檔案

  2. 呼叫so庫檔案:
    so庫檔案生成後,我們需要在app.gradle中宣告so庫的路徑 :

android{
……
 sourceSets{
        main{
            jniLibs.srcDirs = ['libs'] // 宣告路徑
        }
    }
}
  1. ok,大功告成,直接呼叫之前建立的CallUtils類中的callSimpleInfo方法;
    在這裡插入圖片描述

1. ndk-build編譯多個so庫

上面我們已經通過ndk-build生成了當個so庫了,但是如何同時生成多個so庫呢?我們只編寫多個c程式碼原始檔 並修改Android.mk就可以了!

  1. 之前我們編寫了use_ndk_build.c檔案,我們再編寫一個use_ndk_build2.c檔案,程式碼如下:
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild_CallUtils_callSimpleInfo2
  (JNIEnv *env, jclass ojb){
     return (*env) -> NewStringUTF(env,"I'm an info come from use libuse_ndk_build2.so");
  };
  1. 在Android.mk檔案下再新增一個模組,修改後的Android.mk程式碼:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 匯出的so庫名字
LOCAL_MODULE := use_ndk_build
# 對應的c程式碼
LOCAL_SRC_FILES := use_ndk_build.c
include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := use_ndk_build2
LOCAL_SRC_FILES := use_ndk_build2.c
include $(BUILD_SHARED_LIBRARY)
  1. 開啟Terminal,執行ndk-build命令,成功後,libs每個CPU目錄下就能看到libuse_ndk_build.so和libuse_ndk_build2.so兩個so庫檔案了!大功告成。

關於Android.mk的使用介紹我計劃重新寫了一篇文章進行說明;

3. ndk-build將多個.c / .c++ 檔案編譯成一個so

前面多個so檔案我們在Android.mk中新增多個模組,如果多個.c / .c++ 編譯成一個so,我們只需要將多個c原始碼檔案放到LOCAL_SRC_FILES 目錄下即可。

實現:修改Android.mk裡面的:LOCAL_SRC_FILES := 檔案1.c 檔案2.c,檔名和檔名之間需要新增空格,不然會報錯!執行ndk-build,就會發現只有一個so庫檔案了!

3. 通過CMake編譯生成so庫檔案

1. CMake編譯單個so庫

通過CMake編譯so檔案最簡單的例子就是通過Android Studio建立一個InClude C++ support的專案,後面Exceptions Support(-fexception)和Runtime Type Information Support(-frtti)均打上勾;建立完成,編譯執行一下專案。找到app/build/intermediates/cmake/debug資料夾,在這裡我們可以找到剛剛通過CMake生成的對應so檔案;

如果不是新專案,如何在原有專案中通過CMake來實現對C的支援,我們簡單分如下幾步:

  1. 前面的步驟和通過ndk-build生成so庫檔案一直,同樣的方法生成.h 檔案,這裡就不多重複了!生成.h檔案後, 在main目錄下建立cpp資料夾,複製.h檔案到cpp目錄下,並重命名為aserbao-one-lib.cpp。程式碼如下:
#include <jni.h>
#include <string>
JNIEXPORT jstring JNICALL Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useCmake_AserbaoUtils_getSimpleInfoFromOne
        (JNIEnv *env, jclass){
    std::string hello = "這是來自第一個包的資訊,請注意查收……";
    return env->NewStringUTF(hello.c_str());
};
  1. 建立CMakeList.txt檔案;修改成自己關於cpp的檔案路徑(文中的src/main/cpp/aserbao-one-lib.cpp)及生成的so庫名(文中的 use_cmake_build);
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
            use_cmake_build
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/aserbao-one-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib
              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       use_cmake_build

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
  1. 在app.gradle中宣告CMakeLists.txt路徑及配置需要生成的CPU型別,配置如下:
android{
	……
	defaultConfig{
		……
		externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                abiFilters"armeabi","armeabi-v7a","arm64-v8a","mips","mips64","x86","x86_64"
            }
        }
	}
	externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
  1. 編譯執行專案;編譯完成到app/build/intermediates/cmake/debug目錄下,就能看到編譯完成的so庫檔案了!

1. CMake編譯多個so庫

上面我們有使用ndk-build同時編寫過多個so庫檔案,其實CMake的同時編寫多個so原理是一樣的,新增cpp程式碼,修改CMakeLists.txt的檔案。

編寫c檔案程式碼就不多介紹了,程式碼可以下載文章的專案進行檢視:

簡單講解下CMakeLists.txt的修改,複製一份add_library(……)和target_link_libraries(……)修改其對應的庫名字和c程式碼路徑即可,修改後的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
             use_cmake_build

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/num_one/aserbao-one-lib.cpp )

add_library( # Sets the name of the library.
             use_cmake_build2

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/num_two/aserbao-two-lib.cpp )


find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

target_link_libraries( # Specifies the target library.
                       use_cmake_build

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

target_link_libraries( # Specifies the target library.
                       use_cmake_build2

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

OK,重新build專案,在app/build/intermediates/cmake/debug目錄對應的CPU目錄下就能看到libuse_cmake_build.so和libuse_cmake_build2.so檔案
大功告成!

3. 使用CMake將多個c / c++原始碼編譯成一個so

前面講過ndk-build如何將多個c原始碼編譯成一個so,其實CMake的編譯生成一個so的原理也是一樣的,就是將在原本的.cpp路徑後面再新增其他的.cpp路徑,留言檔案之間用空格鍵隔開!

//只需要修改add_library下的c程式碼路徑:
add_library( # Sets the name of the library.
             use_cmake_build

             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/num_one/aserbao-one-lib.cpp src/main/cpp/num_two/aserbao-two-lib.cpp)

修改完成,重新編譯,檢視so庫!一氣呵成,大功告成!

如何呼叫so檔案?

封裝對應的路徑類檔案到指定目錄下

簡單說下原理:首先我們編譯的C程式碼方法命名:Java_路徑_類名_方法名;我們在編譯成so檔案之後,只有在路徑下的類名才能呼叫到so裡面的方法。
下面我們來舉例說明:
這是我的本地訪問類CallUtils的路徑:com.aserbao.aserbaosandroid.functions.how_create_so.useNdkBuild.CallUtils;
對應的c程式碼中的方法為:

#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild_CallUtils_callSimpleInfo
  (JNIEnv *env, jclass ojb){
     return (*env) -> NewStringUTF(env,"Hello, I'm an info come from use ndk-build");
  };

所以編譯生成的so庫檔案只有com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild路徑下的CallUtils才能呼叫!

所以我們其他專案中呼叫so庫的時候,必須要建立對應的訪問類CallUtils,且其路徑和so中定義的路徑一致。即上文中的com.aserbao.aserbaosandroid.functions.how_create_so.useNdkBuild.CallUtils。

ok,搞定,大功告成,不過這種方式的侷限性很高,每次呼叫一個so庫檔案,都需要建立不同的路徑呼叫檔案,所以,為了方便,我們可以通過生成可執行jar的方式來呼叫so庫檔案。

so檔案的Java類封裝成可執行的jar檔案

前面在ndk-build建立so檔案的時候,我們有建立一個類CallUtils,我們基於此基礎來進行:

  1. 我們在其目錄下執行javac CallUtils.java 命令,生成CallUtils.class檔案;
  2. 我們在java目錄下建立名為MANIFEST.MF的檔案。並編輯器內容為:
Main-Class: com.aserbao.aserbaosandroid.functions.how_create_so.useNdkBuild.CallUtils

  1. 開啟Termainal,切換到java目錄下。輸入
 jar cvfm aserbao.jar MANIFEST.MF com/aserbao/aserbaosandroid/functions/how_create_so/useNdkBuild/CallUtils.class
  1. 執行成功後,在java目錄下會生成一個aserbao.jar的檔案,這個檔案就是我們的可執行jar檔案。現在我們將生成的so檔案和這個jar檔案移到任意一個專案的libs中,重新整理下專案,我們就可以通過CallUtils來呼叫生成的so檔案內容了!nice,結束。

注意:

  1. MANIFEST.MF的命名必須是MANIFEST.MF;
  2. MANIFEST.MF的內容組成為:M(必須大寫)ain-C(必須大寫)lass:(空格)包名.類名(必須回車換行)
  3. 執行的類名必須是當前類生成的對應.class檔案

如何檢視so庫中的方法

上面我們講了如何呼叫so庫,其中必不可少的部分是C程式碼宣告的路徑及呼叫類名和方法名。但是當我們只拿到so庫檔案,沒有其他任何資訊,我們如何拿到so庫中宣告的方法路徑及可呼叫的類名呢?下面我們簡單講解下:
在linux下檢視動態庫和靜態庫:

//靜態庫用
ar -t YourFile
//動態庫用 
nm -D YourFile

比如我要檢視我自己編譯生成的libuse_ndk_build.so庫中的方法,我們可以到其目錄下執行如下命令:

nm -D libuse_ndk_build.so

得到的返回結果如下:

000000000000062c T Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild_CallUtils_callSimpleInfo
0000000000000640 T Java_com_aserbao_aserbaosandroid_functions_how_1create_1so_useNdkBuild_CallUtils_callSimpleInfo2
……

兩個方法都顯示出來了,nice,我們根據路徑編譯對應的類檔案及方法名就可以呼叫這個so了,前面已經講過了,這裡就不多說了!

專案地址

AserbaosAndroid
aserbao的個人Android總結專案,希望這個專案能成為最全面的Android開發學習專案,這是個美好的願景,專案中還有很多未涉及到的地方,有很多沒有講到的點,希望看到這個專案的朋友,如果你在開發中遇到什麼問題,在這個專案中沒有找到對應的解決辦法,希望你能夠提出來,給我留言或者在專案github地址提issues,我有時間就會更新專案沒有涉及到的部分!專案會一直維護下去。當然,我希望是Aserbao’sAndroid 能為所有Android開發者提供到幫助!也期望更多Android開發者能參與進來,只要你熟悉Android某一塊,都可以將你的程式碼pull上分支供大家學習!

總結

沒啥好總結的,搞得我腦殼疼!寫一篇文章容易嘛,還要搞清楚各個點,有問題還要幫忙解決,沒啥好總結的!好了,就這樣。如果關注我的朋友在開發過程中遇到相關問題可以在公眾號給我留言,知無不言。撤了撤了……

參考文件