1. 程式人生 > >C語言基礎及指標⑩預編譯及jni.h分析

C語言基礎及指標⑩預編譯及jni.h分析

接續上篇C語言基礎及指標⑨聯合體與列舉

在上篇中我們瞭解了 , 多型別集合的聯合體 , 固定值集合的列舉 , 內容相對比較簡單 , 今天我們談談預編譯 , 也是本系列最後一個知識點 , C語言基礎系列就要告一段落了 , 要開始我們的jni系列了 , JNI(Java Native Interface) 是java與C/C++進行通訊的一種技術 , 使用JNI技術,可以java呼叫C/C++的函式物件等等,Android中的Framework層與Native層就是採用的JNI技術 。

預編譯

預編譯(預處理,巨集定義,巨集替換)這種叫法 , 關鍵字#define , 其本質是替換文字。

首先我們瞭解一下C語言的執行過程:

  1. 編譯 --> 生成目的碼(.obj)
  2. 連線 --> 將目的碼與C函式庫合併 , 生成最終可執行檔案
  3. 執行

預編譯 , 主要在編譯時期完成文字替換工作 , 常見的預編譯指令有: #include,ifndef,#endif,define,#pragma once等等 。

我們在jni.h標頭檔案中 , 可以看到較多的預編譯指令 , 例如:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

如果編譯環境是C++, 則使用:

typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;

C語言編譯環境 , 則使用:

typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;

這裡的定義就是我們後需要結束的JNIEnv指標 , 在C++環境中JNIEnv是一個一級指標 , 但是在C語言環境中 , 他是一個二級指標 ,這個我們將在jni系列中 , 再詳細說明 。

預編譯示例

// 定義一個常數
#define MAX 100 

void main() {

    int i = 99;
    if (i < MAX) // 在編譯時期, 會將MAX替換成100
    {
        printf("i 小於 MAX\n");
    }
}

定義一個巨集常量MAX他的代表100 , 巨集常數沒有型別 , 只做替換。

巨集函式

#define 預編譯指令 , 不光可以定義常量 , 還可以定義函式 , 因為其本質是替換 , 所有可以簡化很多很長的函式名稱 。

在jni.h中 , 也可以看到巨集函式 , 如下:

#define CALL_TYPE_METHODA(_jtype, _jname)                                   \
    __NDK_FPABI__                                                           \
    _jtype Call##_jname##MethodA(jobject obj, jmethodID methodID,           \
        jvalue* args)                                                       \
    { return functions->Call##_jname##MethodA(this, obj, methodID, args); }

其中(_jtype, _jname)裡面的_jtype , _jname都是替換標識 , 替換_jtype,##_jname##

巨集函式示例

// 普通函式
void _jni_define_func_read() {
    printf("read\n");
}

void _jni_define_func_write() {
    printf("write\n");
}

// 定義巨集函式
#define jni(NAME) _jni_define_func_##NAME() ;

void main() {

    // 巨集函式
    //jni(read); 可以簡化函式名稱
    jni(write) ;

    system("pause");
}

巨集函式的核心就是替換 , 只要記住這一點就夠了 。下面我們就來做另一個例項 , 列印類似android Log日誌的形式的函式。

// 模擬Android日誌輸出 , 核心就是替換
#define LOG(LEVE,FORMAT,...) printf(##LEVE); printf(##FORMAT,__VA_ARGS__) ;
#define LOGI(FORMAT,...) LOG("INFO:",##FORMAT,__VA_ARGS__) ;
#define LOGE(FORMAT,...) LOG("ERROR:",##FORMAT,__VA_ARGS__) ;
#define LOGW(FORMAT,...) LOG("WARN:",##FORMAT,__VA_ARGS__) ;

void main() {

    LOGI("%s", "自定義日誌。。。。\n");
    LOGE("%s", "我是錯誤日誌...\n");

    system("pause");
}

輸出:

INFO:自定義日誌。。。。
ERROR:我是錯誤日誌...

我們可以看到輸出資訊前面帶有型別標識 。在這裡做幾點程式碼說明:

  1. 上述程式碼中LEVE,FORMAT都是自定義的 , 可以是任意名稱 , 只有後面替換的名稱一致即可。
  1. LOG(LEVE,FORMAT,...)中的...表示可變引數 , 替換則是使用__VA_ARGS__這種固定寫法 。
  2. 巨集函式的核心就是——替換

預編譯指令就介紹到這裡 , 其中心點就是替換 。 學到這裡 , C語言的基礎也介紹得七七八八了 , 下面一起來分析分析 , 我們編寫JNI程式碼的一個比較重要的標頭檔案jni.h 。

簡要分析jni.h

jni.h我們編寫NDK的時候需要引入的一個頭檔案 , 它位於android-ndk-r10e\platforms\android-21\arch-arm\usr\include\jni.h不同的平臺都有相應的jni.h 。

在檔案開始部分 , 定義了很多類型別名 , 區分C++與C的編譯環境 , 如下:

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};

#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;

接著定義了物件引用的列舉 , 和方法簽名的結構體:

typedef enum jobjectRefType {
    JNIInvalidRefType = 0,
    JNILocalRefType = 1,
    JNIGlobalRefType = 2,
    JNIWeakGlobalRefType = 3
} jobjectRefType;

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

接下來就是最重要的JNIEnv了 , 區分了C和C++環境

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

JNIEnv是一個結構體指標 , 裡面定義了很多函式 , 有呼叫java方法的函式 , 轉換java型別的函式 , 我可以看到 , C編譯環境中 , JNIEnv是struct JNINativeInterface*的指標別名,我們來看看JNINativeInterface結構體是怎樣的:

/*
 * Table of interface function pointers.
 */
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);

    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
                        jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);

    jmethodID   (*FromReflectedMethod)(JNIEnv*, jobject);
    jfieldID    (*FromReflectedField)(JNIEnv*, jobject);
    /* spec doesn't show jboolean parameter */
    jobject     (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);

    jclass      (*GetSuperclass)(JNIEnv*, jclass);
    jboolean    (*IsAssignableFrom)(JNIEnv*, jclass, jclass);

大部分的操作都是在JNINativeInterface這個結構體裡面,定義了很多操作函式 , 比較常見的字元處理函式:

jstring     (*NewStringUTF)(JNIEnv*, const char*);
jsize       (*GetStringUTFLength)(JNIEnv*, jstring);
/* JNI spec says this returns const jbyte*, but that's inconsistent */
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
  1. 將字元指標轉換成java的String型別
  1. 得到java的String型別長度
  2. 將java的String型別轉換成C的字元指標

值得注意的是 , C++的JNIEnv結構體指標 , 並沒有重新實現 , 而生將C中的JNINativeInterface結構體 , 重新打了一次包 , 如下:

/*
 * C++ object wrapper.
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }

因為C++是面嚮物件語言 , 所有有上下文環境變數this , 簡化了C語言中呼叫函式需要傳遞本身結構體指標的操作 。

分析了部分jni.h的原始碼 , 後續的原始碼 , 我相信看完C語言系列, 也可以看得懂 , 這裡就不再做分析了 , 讀者可自行檢視原始碼分析一遍 ,對寫jni程式碼還是有好處的 , 至少對JNIEnv結構體指標中的函式有一個大致的印象 。

結語

C語言基礎系列 , 就宣告完結了 , 還有很多知識點沒講到 , 還有很多需要學習 , 但我們不是要把C語言學透了學精了 , 再去學JNI 和NDK, 這樣 , 不知要到何年何月了 。 所以 , 先學基礎瞭解概貌 , 將java與C結合起來 , 著手做些東西出來 , 然後再繼續深入 。

語言都是相通的 , 關鍵是解決問題的思路 。

歡迎加入Android開發技術交流QQ群:150923287,本群可免費獲取Flutter、Gradle、RxJava、小程式、Hybrid、移動架構、NDK、React Native、效能優化等技術教程!!!