Android NDK-深刻理解JNI

Java調用C/C++在Java語言裏面原本就有的,並不是Android獨有的,即JNI。JNI就是Java調用C++的規範。html

JNI 概述

JNI,全稱爲Java Native Interface,即Java本地接口,JNI是Java調用Native語言的一種特性,經過JNI能夠使JAVA和 C/C++進行交互。
Java語言是跨平臺的語言,而這跨平臺的背後都是依靠Java虛擬機,虛擬機採用C/C++編寫,適配各個系統,經過JNI爲上層Java提供各類服務,保證跨平臺性。
在Java語言出現前,就有不少程序和庫都是由Native語言寫的,若是想重複利用這些庫,就能夠所使用JNI來實現。在Android平臺上,JNI就是一座將Java世界和Native世界聯通的一座橋樑。
java

jni.png
經過JNI,Java世界和Native世界的代碼就能夠相互訪問了。

JNI實例:Camera

最新有在看系統的Camera相關,因此從系統Camera角度來分析下JNI的應用,下面講的實例基於Camera2android

Android5.0(21)以後android.hardware.Camera就被廢棄了,取而代之的是全新的android.hardware.Camera2c++

相關代碼:git

frameworks/base/core/jni/AndroidRuntime.cpp

frameworks/base/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp
複製代碼

Camera2 Java層對應的是CameraMetadataNative.java,Native層對應的是android_hardware_camera2_CameraMetadata.cpp數組

Java層CameraMetadataNative

相關代碼在CameraMetadataNative.java
Camera2使用CameraManager(攝像頭管理器)進行控制,CameraManager具體的操做會經過CameraMetadataNative來執行。
CameraMetadataNative的初始化bash

public class CameraMetadataNative implements Parcelable static {
      /* * We use a class initializer to allow the native code to cache some field offsets */
      nativeClassInit();
      registerAllMarshalers();
   }
   private static native void nativeClassInit();
}
複製代碼

靜態方法初始化調用了Native層的方法nativeClassInit,這個方法對應的Native層具體實現,是在android_hardware_camera2_CameraMetadata.cpp數據結構

Native層CameraMetadata

Native層相關代碼在android_hardware_camera2_CameraMetadata.cpp
Native方法初始化jvm

static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
  { "nativeClassInit",
    "()V",
    (void *)CameraMetadata_classInit },   //和Java層nativeClassInit()對應
  { "nativeGetAllVendorKeys",
    "(Ljava/lang/Class;)Ljava/util/ArrayList;",
    (void *)CameraMetadata_getAllVendorKeys},
  { "nativeGetTagFromKey",
    "(Ljava/lang/String;)I",
    (void *)CameraMetadata_getTagFromKey },
  { "nativeGetTypeFromTag",
    "(I)I",
    (void *)CameraMetadata_getTypeFromTag },
  { "nativeSetupGlobalVendorTagDescriptor",
    "()I",
    (void*)CameraMetadata_setupGlobalVendorTagDescriptor },
// instance methods
  { "nativeAllocate",
    "()J",
    (void*)CameraMetadata_allocate },
  { "nativeAllocateCopy",
    "(L" CAMERA_METADATA_CLASS_NAME ";)J",
    (void *)CameraMetadata_allocateCopy },
  { "nativeIsEmpty",
    "()Z",
    (void*)CameraMetadata_isEmpty },
  { "nativeGetEntryCount",
    "()I",
    (void*)CameraMetadata_getEntryCount },
  { "nativeClose",
    "()V",
    (void*)CameraMetadata_close },
  { "nativeSwap",
    "(L" CAMERA_METADATA_CLASS_NAME ";)V",
    (void *)CameraMetadata_swap },
  { "nativeReadValues",
    "(I)[B",
    (void *)CameraMetadata_readValues },
  { "nativeWriteValues",
    "(I[B)V",
    (void *)CameraMetadata_writeValues },
  { "nativeDump",
    "()V",
    (void *)CameraMetadata_dump },
// Parcelable interface
  { "nativeReadFromParcel",
    "(Landroid/os/Parcel;)V",
    (void *)CameraMetadata_readFromParcel },
  { "nativeWriteToParcel",
    "(Landroid/os/Parcel;)V",
    (void *)CameraMetadata_writeToParcel },
};
複製代碼

gCameraMetadataMethods何時會被加載?ide

int register_android_hardware_camera2_CameraMetadata(JNIEnv *env) {
   ......
   // Register native functions
   return RegisterMethodsOrDie(env,
         CAMERA_METADATA_CLASS_NAME,
         gCameraMetadataMethods,
         NELEM(gCameraMetadataMethods));
}
......
static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
    int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);
    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
    return res;
}
複製代碼

register_android_hardware_camera2_CameraMetadata什麼時候會被調用到,這個就須要瞭解下JNI的查找方式。

JNI查找方式

Android系統在啓動啓動過程當中,先啓動Kernel建立init進程,緊接着由init進程fork第一個橫穿Java和C/C++的進程,即Zygote進程。Zygote啓動過程當中會AndroidRuntime.cpp中的startVm建立虛擬機,VM建立完成後,緊接着調用startReg完成虛擬機中的JNI方法註冊。

剛纔CameraMetadata中register_android_hardware_camera2_CameraMetadata方法,在AndroidRuntime.cpp的聲明:

extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env);
複製代碼

而後在gRegJNI中的靜態聲明

static const RegJNIRec gRegJNI[] = {
    ......
    REG_JNI(register_android_hardware_camera2_CameraMetadata),
    ......
}
複製代碼

gRegJNI方法在startReg中被調用

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
    ALOGV("--- registering native functions ---\n");
  
    env->PushLocalFrame(200);

    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);

    //createJavaThread("fubar", quickTest, (void*) "hello");
    return 0;
}
複製代碼

register_jni_procs(gRegJNI, NELEM(gRegJNI), env)會循環調用gRegJNI數組成員所對應的方法

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) {
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
            ALOGD("----------!!! %s failed to load\n", array[i].mName);
#endif
            return -1;
        }
    }
    return 0;
}
複製代碼

這樣android_hardware_camera2_CameraMetadata.cpp中的int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)就會被調用到。
除了這種Android系統啓動時,就註冊JNI所對應的方法。還有一種就是程序自定義的JNI方法,以 MediePlay 爲例: 相關代碼路徑

frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp
複製代碼

MediaPlayer聲明:

public class MediaPlayer extends PlayerBase implements SubtitleController.Listener {
   ......
   private static native final void native_init();
   ......
   static {
      System.loadLibrary("media_jni");
      native_init();
   }
}
複製代碼

靜態代碼塊中使用System.loadLibrary加載動態庫,media_jni在Android平臺對應的是libmedia_jni.so庫。
在jni目錄/frameworks/base/media/jni/Android.mk中有相應的聲明:

LOCAL_SRC_FILES:= \
android_media_MediaPlayer.cpp \
...
LOCAL_MODULE:= libmedia_jni
複製代碼

android_media_MediaPlayer.cpp找到對應的Native(natvie_init)方法:

static void android_media_MediaPlayer_native_init(JNIEnv *env) {
    jclass clazz;

    clazz = env->FindClass("android/media/MediaPlayer");
    if (clazz == NULL) {
        return;
    }
    ......
}
複製代碼

JNI註冊的方法就是上面描述的兩種方法:

  • 在Android系統啓動時註冊,在AndroidRuntime.cpp中的gRegJNI方法中聲明
  • 使用System.loadLibrary()方式註冊

JNI基礎

上面一節主要描述了系統中Java層和Native層交互和實現,並無對JNI的基礎理論,流程進行分析

JNI命名規則

JNI方法名規範 :

返回值 + Java前綴 + 全路徑類名 + 方法名 + 參數① JNIEnv + 參數② jobject + 其它參數
複製代碼

簡單的一個例子,返回一個字符串

extern "C" JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI(JNIEnv *env, jclass jclass1) {
    LOGD("##### from c");

    return env->NewStringUTF("Hello JNI");
}
複製代碼
  • 返回值:jstring
  • 全路徑類名:com_yeungeek_jnisample_NativeHelper
  • 方法名:stringFromJNI

JNI開發流程

  • 在Java中先聲明一個native方法
  • 編譯Java源文件javac獲得.class文件
  • 經過javah -jni命令導出JNI的.h頭文件
  • 使用Java須要交互的本地代碼,實如今Java中聲明的Native方法(若是Java須要與C++交互,那麼就用C++實現Java的Native方法。)
  • 將本地代碼編譯成動態庫(Windows系統下是.dll文件,若是是Linux系統下是.so文件,若是是Mac系統下是.jnilib)
  • 經過Java命令執行Java程序,最終實現Java調用本地代碼。

數據類型

基本數據類型

Signature Java Native
B byte jbyte
C char jchar
D double jdouble
F float jfloat
I int jint
S short jshort
J long jlong
Z boolean jboolean
V void jvoid

引用數據類型

Signature Java Native
L+classname +; Object jobject
Ljava/lang/String; String jstring
[L+classname +; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable
[B byte[] jbyteArray
[C char[] jcharArray
[D double[] jdoubleArray
[F float[] jfloatArray
[I int[] jintArray
[S short[] jshortArray
[J long[] jlongArray
[Z boolean[] jbooleanArray

方法簽名

JNI的方法簽名的格式:

(參數簽名格式...)返回值簽名格式
複製代碼

demo的native 方法:

public static native java.lang.String stringFromJNI();
複製代碼

能夠經過javap命令生成方法簽名``:

()Ljava/lang/String;
複製代碼

JNI原理

Java語言的執行環境是Java虛擬機(JVM),JVM實際上是主機環境中的一個進程,每一個JVM虛擬機都在本地環境中有一個JavaVM結構體,該結構體在建立Java虛擬機時被返回,在JNI環境中建立JVM的函數爲JNI_CreateJavaVM。
JNI 定義了兩個關鍵數據結構,即「JavaVM」和「JNIEnv」,二者本質上都是指向函數表的二級指針。

JavaVM

JavaVM是Java虛擬機在JNI層的表明,JavaVM 提供了「調用接口」函數,您能夠利用此類函數建立和銷燬 JavaVM。理論上,每一個進程能夠包含多個JavaVM,但AnAndroid只容許每一個進程包含一個JavaVM。

JNIEnv

JNIEnv是一個線程相關的結構體,該結構體表明瞭Java在本線程的執行環境。JNIEnv 提供了大多數 JNI 函數。您的原生函數均會接收 JNIEnv 做爲第一個參數。
JNIEnv做用:

  • 調用Java函數
  • 操做Java代碼

JNIEnv定義(jni.h): libnativehelper/include/nativehelper/jni.h

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

定義中能夠看到JavaVM,Android中一個進程只會有一個JavaVM,一個JVM對應一個JavaVM結構,而一個JVM中可能建立多個Java線程,每一個線程對應一個JNIEnv結構

javavm.png

註冊JNI函數

Java世界和Native世界的方法是如何關聯的,就是經過JNI函數註冊來實現。JNI函數註冊有兩種方式:

靜態註冊

這種方法就是經過函數名來找對應的JNI函數,能夠經過javah命令行來生成JNI頭文件

javah com.yeungeek.jnisample.NativeHelper
複製代碼

生成對應的com_yeungeek_jnisample_NativeHelper.h文件,生成對應的JNI函數,而後在實現這個函數就能夠了

/* * Class: com_yeungeek_jnisample_NativeHelper * Method: stringFromJNI * Signature: ()Ljava/lang/String; */
JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI (JNIEnv *, jclass);
複製代碼

靜態註冊方法中,Native是如何找到對應的JNI函數,在JNI查找方式中介紹系統的流程,並無詳細說明靜態註冊的查找。這裏簡單說明下這個過程(以上面的聲明爲例子s):
當Java調用native stringFromJNI函數時,會從對應JNI庫中查找Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI函數,若是沒有找到,就會報錯。
靜態註冊方法,就是根據函數名來關聯Java函數和JNI函數,JNI函數須要遵循特定的格式,這其中就有一些缺點:

  • 聲明瞭native方法的Java類,須要經過javah來生成頭文件
  • JNI函數名稱很是長
  • 第一次調用native函數,須要經過函數名來搜索關聯對應的JNI函數,效率比較低

如何解決這些問題,讓native函數,提早知道JNI函數,就能夠解決這個問題,這個過程就是動態註冊。

動態註冊

動態註冊在前面的Camera例子中,已經有涉及到,JNI函數classInit的聲明。

static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
  { "nativeClassInit",
    "()V",
    (void *)CameraMetadata_classInit },   //和Java層nativeClassInit()對應
   ......
}
複製代碼

JNI中有一種結構用來記錄Java的Native方法和JNI方法的關聯關係,它就是JNINativeMethod,它在jni.h中被定義:

typedef struct {
    const char* name;  //Java層native函數名
    const char* signature; //Java函數簽名,記錄參數類型和個數,以及返回值類型
    void*       fnPtr; //Native層對應的函數指針
} JNINativeMethod;
複製代碼

JNI查找方式說到,JNI註冊的兩種時間,第一種已經介紹過了,咱們自定義的native函數,基本都是會使用System.loadLibrary(「xxx」),來進行JNI函數的關聯。

loadLibrary(Android7.0)

public static void loadLibrary(String libname) {
   Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
複製代碼

調用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法:

synchronized void loadLibrary0(ClassLoader loader, String libname) {
   ......
   String libraryName = libname;
   if (loader != null) {
      String filename = loader.findLibrary(libraryName);
      if (filename == null) {
            // It's not necessarily true that the ClassLoader used
            // System.mapLibraryName, but the default setup does, and it's
            // misleading to say we didn't find "libMyLibrary.so" when we
            // actually searched for "liblibMyLibrary.so.so".
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                          System.mapLibraryName(libraryName) + "\"");
      }
      //doLoad
      String error = doLoad(filename, loader);
      if (error != null) {
            throw new UnsatisfiedLinkError(error);
      }
      return;
   }
   //loader 爲 null
   ......
   for (String directory : getLibPaths()) {
      String candidate = directory + filename;
      candidates.add(candidate);

      if (IoUtils.canOpenReadOnly(candidate)) {
            String error = doLoad(candidate, loader);
            if (error == null) {
               return; // We successfully loaded the library. Job done.
            }
            lastError = error;
      }
   }
   ......
}
複製代碼

doLoad

private String doLoad(String name, ClassLoader loader) {
   //調用 native 方法
   synchronized (this) {
      return nativeLoad(name, loader, librarySearchPath);
   }
}
複製代碼

nativeLoad

進入到虛擬機代碼/libcore/ojluni/src/main/native/Runtime.c

JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) {
    return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
複製代碼

而後調用JVM_NativeLoad,JVM_NativeLoad方法申明在jvm.h中,實如今OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)

JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) {
  ScopedUtfChars filename(env, javaFilename);
  if (filename.c_str() == NULL) {
    return NULL;
  }

  std::string error_msg;
  {
    art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
    bool success = vm->LoadNativeLibrary(env,
                                         filename.c_str(),
                                         javaLoader,
                                         javaLibrarySearchPath,
                                         &error_msg);
    if (success) {
      return nullptr;
    }
  }

  // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
  env->ExceptionClear();
  return env->NewStringUTF(error_msg.c_str());
}
複製代碼

LoadNativeLibrary

調用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java_vm_ext.cc)中,這個方法代碼很是多,選取主要的部分進行分析

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                  const std::string& path,
                                  jobject class_loader,
                                  jstring library_path,
                                  std::string* error_msg) {
         ......
         bool was_successful = false;
         //加載so庫中查找JNI_OnLoad方法,若是沒有系統就認爲是靜態註冊方式進行的,直接返回true,表明so庫加載成功,
         //若是找到JNI_OnLoad就會調用JNI_OnLoad方法,JNI_OnLoad方法中通常存放的是方法註冊的函數,
         //因此若是採用動態註冊就必需要實現JNI_OnLoad方法,不然調用java中申明的native方法時會拋出異常
         void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
         if (sym == nullptr) {
            VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
            was_successful = true;
         } else {
            // Call JNI_OnLoad. We have to override the current class
            // loader, which will always be "null" since the stuff at the
            // top of the stack is around Runtime.loadLibrary(). (See
            // the comments in the JNI FindClass function.)
            ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
            self->SetClassLoaderOverride(class_loader);

            VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
            typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
            JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
            //調用JNI_OnLoad方法
            int version = (*jni_on_load)(this, nullptr);

            if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
               // Make sure that sigchain owns SIGSEGV.
               EnsureFrontOfChain(SIGSEGV);
            }

            self->SetClassLoaderOverride(old_class_loader.get());
         }
         ......
}                        
複製代碼

代碼裏的主要邏輯:

  • 加載so庫中查找JNI_OnLoad方法,若是沒有系統就認爲是靜態註冊方式進行的,直接返回true,表明so庫加載成功
  • 若是找到JNI_OnLoad就會調用JNI_OnLoad方法,JNI_OnLoad方法中通常存放的是方法註冊的函數
  • 因此若是採用動態註冊就必需要實現JNI_OnLoad方法,不然調用Java中的native方法時會拋出異常

jclass、jmethodID和jfieldID

若是要經過原生代碼訪問對象的字段,須要執行如下操做:

  1. 使用 FindClass 獲取類的類對象引用
  2. 使用 GetFieldID 獲取字段的字段 ID
  3. 使用適當內容獲取字段的內容,例如 GetIntField

具體的使用,放在第二篇文章中講解

JNI的引用

JNI規範中定義了三種引用:

  • 局部引用(Local Reference)
  • 全局引用(Global Reference)
  • 弱全局引用(Weak Global Reference)

局部引用

也叫本地引用,在 JNI層函數使用的非全局引用對象都是Local Reference,最大的特色就是,JNI 函數返回後,這些聲明的引用可能就會被垃圾回收

全局引用

這種聲明的對象,不會主動釋放資源,不會被垃圾回收

弱全局引用

一種特殊的全局引用,在運行過程當中可能被回收,使用以前須要判斷下是否爲空

參考

相關文章
相關標籤/搜索