接續上篇C語言基礎及指針⑨聯合體與枚舉
在上篇中我們了解了 , 多類型集合的聯合體 , 固定值集合的枚舉 , 內容相對比較簡單 , 今天我們談談預編譯 , 也是本系列最后一個知識點 , C語言基礎系列就要告一段落了 , 要開始我們的jni系列了 , JNI(Java Native Interface) 是java與C/C++進行通信的一種技術 , 使用JNI技術,可以java調用C/C++的函數對象等等,Android中的Framework層與Native層就是采用的JNI技術 。
預編譯
預編譯(預處理,宏定義,宏替換)這種叫法 , 關鍵字
#define
, 其本質是替換文本。
首先我們了解一下C語言的執行過程:
- 編譯 --> 生成目標代碼(.obj)
- 連接 --> 將目標代碼與C函數庫合并 , 生成最終可執行文件
- 執行
預編譯 , 主要在編譯時期完成文本替換工作 , 常見的預編譯指令有:
#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:我是錯誤日志...
我們可以看到輸出信息前面帶有類型標識 。在這里做幾點代碼說明:
- 上述代碼中
LEVE
,FORMAT
都是自定義的 , 可以是任意名稱 , 只有后面替換的名稱一致即可。
-
LOG(LEVE,FORMAT,...)
中的...
表示可變參數 , 替換則是使用__VA_ARGS__
這種固定寫法 。 - 宏函數的核心就是——替換
預編譯指令就介紹到這里 , 其中心點就是替換
。 學到這里 , 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*);
- 將字符指針轉換成java的String類型
- 得到java的String類型長度
- 將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程序員學C系列:
C語言基礎及指針①
C語言基礎及指針②之指針內存分析
C語言基礎及指針③函數與二級指針
C語言基礎及指針④函數指針
C語言基礎及指針⑤動態內存分配
C語言基礎及指針⑥字符操作
C語言基礎及指針⑦結構體與指針
C語言基礎及指針⑧文件IO
C語言基礎及指針⑨聯合體與枚舉
C語言基礎及指針⑩預編譯及jni.h分析