Eclipse中使用JNI/NDK實現C代碼調用Java方法

概述

C調用Java方法一般是通過反射來實現的,和Java中的反射相似主要分爲三個步驟:

1,通過反射拿到字節碼對象

2,獲取方法的方法ID

3,通過反射調用Java方法

開發環境

Eclipse + ADT + AndroidSDK + NDK

注意NDK版本不要太高,我用的是NDK r10e

創建項目

創建Android項目:項目名爲CCallJava,主要是流程通過點擊button調用C函數,C中再調用Java方法顯示一個對話框;

配置NDK

Eclipse的window選項中選擇Preferences,點擊左邊Android展開子選項,點擊NDK,在右邊NDK location中輸入自己的NDK路徑,或者點擊Browser選擇找到NDK根路徑點擊確認;

自動生成jni文件夾

配置之後可以右鍵項目選在Android Tools —>Add Native Support ...輸入編譯要生成動態庫的名稱也是生成的c++文件的名稱例如:hello,點擊finish;自動生成jni目錄以及默認C++文件和Android.mk文件;這種方式新建之後不能刪除jni,不然就會報錯;

 Android.mk文件內容,前兩行和最後一行一般不用修改;

LOCAL_PATH:= $(call my-dir)


include $(CLEAR_VARS)

LOCAL_MODULE:= hello #動態庫名稱,Java代碼中也是根據這個名稱加載動態庫
LOCAL_SRC_FILES:= hello.c #C文件,裏面就是我們寫的C代碼。自動生成的是hello.cpp這個改成了C文件

include $(BUILD_SHARED_LIBRARY)#生成.so動態庫

#include $(BUILD_STATIC_LIBRARY) 編譯出.a的靜態庫

#注意註釋用#開頭

關聯NDK源碼

右鍵項目—>properties—>C C++General—>Path and Symbols—>Includes—>add —>File System...選擇ndk源碼路徑,例如r10版本:android-ndk-r10e\platforms\android-19\arch-arm\usr\include (注意不同ndk版本include源碼不一樣,比如ndk r17是ndk_r17\sysroot\usr\include) 

新建本地方法

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void click(View v){
    	helloC();//調用本地方法
    }
       
    //本地方法
    public native void helloC();
    
    //C中調用的方法
    public void show(String msg){
    	Builder builder = new Builder(this); //注意導包是android.app.AlertDialog.Builder;
    	builder.setTitle("提示!");
    	builder.setMessage(msg);
    	builder.show();
    }
}

自動生成頭文件

通過javah命令生成本地方法對應的頭文件;

cmd命令中 cd 切換到項目的src目錄下,運行javah + 本地方所在類的全路徑名;

C:\Users\Ang>cd D:\EProject\CCallJava\src

C:\Users\Ang>d:

D:\EProject\CCallJava\src>javah com.example.ccalljava.MainActivity

D:\EProject\CCallJava\src>

 在src目錄下就自動生成一個com_example_ccalljava_MainActivity.h頭文件

#include <jni.h>
/* Header for class com_example_ccalljava_MainActivity */

#ifndef _Included_com_example_ccalljava_MainActivity
#define _Included_com_example_ccalljava_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_ccalljava_MainActivity
 * Method:    helloC
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_ccalljava_MainActivity_helloC
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

實現頭文件的中函數

複製頭文件中的函數到jni下的C文件中,並實現這個函數;

hello.c

#include <jni.h>

//env是個二級指針,JNIEnvb本來就是一個指針
JNIEXPORT void JNICALL Java_com_example_ccalljava_MainActivity_helloC//函數名:Java包名+類名+方法名
  (JNIEnv* env, jobject obj)//這兩個參數必須要寫,JNIEvn* 是java運行環境的地址,可以理解爲Java虛擬機地址;jobject Java中調用本地方的對象
{
	//jclass      (*FindClass)(JNIEnv*, const char*); jni.h中定義的函數指針
    jclass class = (*env)->FindClass(env,"com/example/ccalljava/MainActivity"); //是不是和MainActivity.class.forName(classNmae);類似//(*env)->FindClass表示的邏輯是調用*env結構體中的FindClass函數

    //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);//第三個參數是要反射的方法名;最後一個參數char*是要反射調用的方法的簽名
    jmethodID methodID = (*env)->GetMethodID(env,class,"show","(Ljava/lang/String;)V");

    //   void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); 因爲show返回值是void類型的所以這裏調用CallVoidMethod()函數
    (*env)->CallVoidMethod(env,obj,methodID,(*env)->NewStringUTF(env,"C調用Java方法顯示的彈窗"));
//jstring     (*NewStringUTF)(JNIEnv*, const char*);把C中的字符串轉化爲Java中的字符串;C中的字符串是個char*指針或者說char[]; 

}

注意:FindClass函數第二個參數com/example/ccalljava/MainActivity是斜槓作分割,不是點"."

獲取方法簽名

cmd 命令行中 cd 切換到項目的bin\classes 下,運行javap -s  + 要簽名方法所在類的全路徑名;


C:\Users\Ang>cd  D:\EProject\CCallJava\bin\classes\

C:\Users\Ang>d:

D:\EProject\CCallJava\bin\classes>javap -s com.example.ccalljava.MainActivity  //執行此命令
Compiled from "MainActivity.java"
public class com.example.ccalljava.MainActivity extends android.app.Activity {
  public com.example.ccalljava.MainActivity();
    descriptor: ()V

  protected void onCreate(android.os.Bundle);
    descriptor: (Landroid/os/Bundle;)V

  public void click(android.view.View);
    descriptor: (Landroid/view/View;)V

  public native void helloC();
    descriptor: ()V

  public void show(java.lang.String);
    descriptor: (Ljava/lang/String;)V //show方法的簽名

注意:C函數調用Java方法傳入參數中有中文會報錯,因爲Eclipse創建的hello.c文件默認的時GBK編碼,而Android使用的UTF-8,碼錶不一致導致的;修改hello.c默認的碼錶爲UTF-8就OK了; 

編譯生成.so動態庫

cmd 命令行 cd切換到jni上級目錄或者jni目錄下,輸入ndk-build  注意:要配置ndk-build.cmd的環境變量,才能使用ndk-bulid命令;

C:\Users\Ang>cd D:\EProject\CCallJava

C:\Users\Ang>d:

D:\EProject\CCallJava>ndk-build
Android NDK: Found platform level in ./project.properties. Setting APP_PLATFORM to android-19.
D:/SDK/ndk-r16b-windows-x86_64/build//../build/core/setup-app.mk:81: Android NDK: Application targets deprecated ABI(s): armeabi
D:/SDK/ndk-r16b-windows-x86_64/build//../build/core/setup-app.mk:82: Android NDK: Support for these ABIs will be removed in a future NDK release.
[armeabi-v7a] Install        : libhello.so => libs/armeabi-v7a/libhello.so
[armeabi] Install        : libhello.so => libs/armeabi/libhello.so
[x86] Install        : libhello.so => libs/x86/libhello.so

注意:如果不在Application.mk文件中指定輸出CPU ABI架構的.so,會根據默認的輸出對應的ABI架構.so庫,一般是所有的ABI架構的.so庫;

指定輸出對應的ABI架構的.so庫

Application.mk

APP_ABI := armeabi-v7a armeabi x86

使用.so庫

在Java代碼加載.so動態庫

MainActivity中加載hello.so庫

static{
        System.loadLibrary("hello");
}

自動編譯.so庫

不用在命令行中輸入ndk-build命令,自動完成編譯輸出.so庫

自建Builder方法

右鍵項目選擇properties—>Builders—>New...—>Program—>OK

在Edit Configuration 中 Name 輸入Builders的名字,Location輸入ndk-build.cmd的路徑,Working Directory輸入當前項目${workspace_loc}在Variables...中可以找到; 

Refresh中勾選 Refresh resources upon completion. 

 Build Options 

勾選Specify working set of relevant resources 接着點擊Specify Resources...

選擇當前項目的jni 

這樣每次運行項目的時候都會重新自動生成.so庫,所以打包apk會變慢;

源代碼:https://github.com/lz-ang/CCallJava 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章