1. 程式人生 > >一天掌握Android JNI本地程式設計 快速入門

一天掌握Android JNI本地程式設計 快速入門

一、JNI(Java Native Interface)

       1、什麼是JNI:               JNI(Java Native Interface):java本地開發介面               JNI是一個協議,這個協議用來溝通java程式碼和外部的原生代碼(c/c++)
              外部的c/c++程式碼也可以呼叫java程式碼        2、為什麼使用JNI               效率上 C/C++是本地語言,比java更高效               程式碼移植,如果之前用C語言開發過模組,可以複用已經存在的c程式碼
              java反編譯比C語言容易,一般加密演算法都是用C語言編寫,不容易被反編譯        3、Java基本資料型別與C語言基本資料型別的對應                       3、引用型別對應               
       4、堆記憶體和棧記憶體的概念               棧記憶體:系統自動分配和釋放,                       儲存全域性、靜態、區域性變數,                       在站上分配記憶體叫靜態分配,                       大小一般是固定的               堆記憶體:程式設計師手動分配(malloc/new)和釋放(free/java不用手動釋放,由GC回收),                       在堆上分配記憶體叫動態分配,                       一般硬體記憶體有多大堆記憶體就有多大   二、交叉編譯        1、交叉編譯的概念           交叉編譯即在一個平臺,編譯出另一個平臺能夠執行的二進位制程式碼           主流平臺有: Windows、 Mac os、 Linux           主流處理器: x86、 arm、 mips        2、交叉編譯的原理           即在一個平臺上,模擬其他平臺的特性           編譯的流程: 原始碼-->編譯-->連結-->可執行程式        3、交叉編譯的工具鏈           多個工具的集合,一個工具使用完後接著呼叫下一個工具        4、常見的交叉編譯工具           NDK(Native Development Kit): 開發JNI必備工具,就是模擬其他平臺特性類編譯程式碼的工具           CDT(C/C++ Development Tools): 是Eclipse開發C語言的一個外掛,高亮顯示C語言的語法           Cygwin: 一個Windows平臺的Unix模擬器(可以參考之前部落格Cygwin簡介及使用        5、NDK的目錄結構(可以在Google官網下載NDK開發工具,需要FQ)           docs: 幫助文件           build/tools:linux的批處理檔案           platforms:編譯c程式碼需要使用的標頭檔案和類庫           prebuilt:預編譯使用的二進位制可執行檔案           sample:jni的使用例子           source:ndk的原始碼           toolchains:工具鏈           ndk-build.cmd:編譯打包c程式碼的一個指令,需要配置系統環境變數   三、JNI的第一個例子           好了,準備知識已經完畢,下面開始我們的一個JNI例子。         1、新建一個Android專案,在根目錄下建立 jni資料夾,用於存放 C原始碼。         2、在java程式碼中,建立一個本地方法 getStringFromC 本地方法沒有方法體。
  1. private native String getStringFromC();

            3、在jni中建立一個C檔案,定義一個函式實現本地方法,函式名必須用使用 本地方法的全類名,點改為下劃線。

複製程式碼
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <jni.h>
 4 //方法名必須為本地方法的全類名點改為下劃線,穿入的兩個引數必須這樣寫,
 5 //第一個引數為Java虛擬機器的記憶體地址的二級指標,用於本地方法與java虛擬機器在記憶體中互動
 6 //第二個引數為一個java物件,即是哪個物件呼叫了這個 c方法
 7 jstring Java_com_mwp_jnihelloworld_MainActivity_getStringFromC(JNIEnv* env,
 8                                                                jobject obj){
 9     //定義一個C語言字串
10     char* cstr = "hello form c";
11     //返回值是java字串,所以要將C語言的字串轉換成java的字串
12     //在jni.h 中定義了字串轉換函式的函式指標
13     //jstring   (*NewStringUTF)(JNIEnv*, const char*);
14     //第一種方法:很少用
15     jstring jstr1 = (*(*env)).NewStringUTF(env, cstr);
16     //第二種方法,推薦
17     jstring jstr2 = (*env) -> NewStringUTF(env, cstr);
18     return jstr2;
19 }
複製程式碼

        4、在jni中建立 Android.mk檔案,用於配置 本地方法

  1. 複製程式碼
     LOCAL_PATH := $(call my-dir)
        include $(CLEAR_VARS)
        #編譯生成的檔案的類庫叫什麼名字
        LOCAL_MODULE    := hello
        #要編譯的c檔案
        LOCAL_SRC_FILES := Hello.c
        include $(BUILD_SHARED_LIBRARY)
    複製程式碼

             5、在jni目錄下執行 ndk-build.cmd指令,編譯c檔案

         6、在java程式碼中載入編譯後生成的so類庫,呼叫本地方法,將專案部署到虛擬機器上之後就會發現toast彈出的C程式碼定義的字串,第一個例子執行成功了。
static{
        //載入打包完畢的 so類庫
        System.loadLibrary("hello");
    }

         7、jni打包的C語言類庫預設僅支援 arm架構,需要在jni目錄下建立 Android.mk 檔案新增如下程式碼可以支援x86架構

  1. APP_ABI := armeabi armeabi-v7a x86


    四、JNI常見錯誤

         1、findLibrary returned null:                 CPU平臺不匹配 或者 在載入類庫時,類庫名字寫錯了          2、本地方法找不到:                 忘記載入類庫了 或者 C程式碼中方法名寫錯了     五、javah工具與javap工具          1、javah:  生成本地方法標頭檔案             需要在C/C++模組下才能生效             在JDK1.7中,在src目錄下執行javah 全類名             在JDK1.6中,在bin/classes目錄下執行          2、javap:  列印方法簽名             在C語言中呼叫java的方法需要用到反射,C語言的反射需要一個方法簽名,使用javap能夠生成方法簽名,很熟練的話也可以自己寫方法簽名             在bin/classes目錄下執行 javap -s 全類名              六、使用本地方法加密字串的一個小例子       C語言字串與Java中的字串型別不同,所以需要進行字串型別轉換。       一個重要的思想:C語言計算字串的長度不方便,但是java很方便,只需要呼叫一個length()方法就可以,所以像這種需求,那個語言有優勢就用哪個語言算,算完當做引數傳遞給另一種語言就ok。                       混合語言程式設計這應該是一種非常有用的思想。      Java非常容易被反編譯,所以加密都是用 c語言寫的   複製程式碼
#include <jni.h>
#include <string.h>
//將java字串轉換為c語言字串(工具方法)
char*   Jstring2CStr(JNIEnv*   env,   jstring   jstr)
{
     char*   rtn   =   NULL;
     jclass   clsstring   =   (*env)->FindClass(env,"java/lang/String");
     jstring   strencode   =   (*env)->NewStringUTF(env,"GB2312");
     jmethodID   mid   =   (*env)->GetMethodID(env,clsstring,   "getBytes",   "(Ljava/lang/String;)[B");
     jbyteArray   barr=   (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312");
     jsize   alen   =   (*env)->GetArrayLength(env,barr);
     jbyte*   ba   =   (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
     if(alen   >   0)
     {
      rtn   =   (char*)malloc(alen+1);         //"\0"
      memcpy(rtn,ba,alen);
      rtn[alen]=0;
     }
     (*env)->ReleaseByteArrayElements(env,barr,ba,0);  //
     return rtn;
}
JNIEXPORT jstring JNICALL Java_com_mwp_encodeanddecode_MainActivity_encode
  (JNIEnv * env, jobject obj, jstring text, jint length){
      char* cstr = Jstring2CStr(env, text);
      int i;
      for(i = 0;i<length;i++){
          *(cstr+i) += 1; //加密演算法,將字串每個字元加1
      }
      return (*env)->NewStringUTF(env,cstr);
}
JNIEXPORT jstring JNICALL Java_com_mwp_encodeanddecode_MainActivity_decode
(JNIEnv * env, jobject obj, jstring text, jint length){
     char* cstr = Jstring2CStr(env, text);
     int i;
     for(i = 0;i<length;i++){
         *(cstr+i) -= 1;
     }
     return (*env)->NewStringUTF(env, cstr);
}
複製程式碼

 

七、JNI操作一個數組(引用傳遞)           傳遞陣列其實是傳遞一個堆記憶體的陣列首地址的引用過去,所以實際操作的是同一塊記憶體,           當呼叫完方法,不需要返回值,實際上引數內容已經改變,           Android中很多操作硬體的方法都是這種C語言的傳引用的思路 複製程式碼
 1 public class MainActivity extends Activity {
 2     
 3     static{
 4         System.loadLibrary("encode");
 5     }
 6     int[] array = {1,2,3,4,5};
 7     @Override
 8     protected void onCreate(Bundle savedInstanceState) {
 9         super.onCreate(savedInstanceState);
10         setContentView(R.layout.activity_main);
11     }
12     
13     public void click(View v){
14         encodeArray(array);
15         //不需要返回值,實際操作的是同一塊記憶體,內容已經發生了改變
16         for (int i : array) {
17             System.out.println(i);
18         }
19     }
20     
21     //傳遞陣列其實是傳遞一個堆記憶體的陣列首地址的引用過去,所以實際操作的是同一塊記憶體,
22     //當呼叫完方法,不需要返回值,實際上引數內容已經改變,
23     //Android中很多操作硬體的方法都是這種C語言的傳引用的思路,要非常熟練
24     private native void encodeArray(int[] arr);
25 }
複製程式碼

 

複製程式碼
 1 #include <jni.h>
 2 /*
 3  * Class:     com_mwp_jniarray_MainActivity
 4  * Method:    encodeArray
 5  * Signature: ([I)V
 6  */
 7 JNIEXPORT void JNICALL Java_com_mwp_jniarray_MainActivity_encodeArray
 8   (JNIEnv * env, jobject obj, jintArray arr){
 9      //拿到整型陣列的長度以及第0個元素的地址
10      //jsize       (*GetArrayLength)(JNIEnv*, jarray);
11     int length = (*env)->GetArrayLength(env, arr);
12     // jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
13     int* arrp = (*env)->GetIntArrayElements(env, arr, 0);
14     int i;
15     for(i = 0;i<length;i++){
16         *(arrp + i) += 10; //將陣列中的每個元素加10
17     }
18 }
複製程式碼

 

八、偷用美圖秀秀的C語言本地類庫加深JNI的理解     專案中不需要有c程式碼,只需要有一個編譯過後的類庫供Java呼叫就可以了。     將美圖秀秀的apk檔案解壓縮,將lib目錄下C類庫匯入自己的專案,     反編譯美圖秀秀的apk檔案,將其本地方法類 JNI.java複製到自己的專案     根據本地方法名和引數猜函式的作用及如何使用,     下例呼叫了美圖的一個LOMO美化效果
複製程式碼
 1 public class MainActivity extends Activity {
 2     
 3     static{
 4         //載入美圖秀秀的類庫
 5         System.loadLibrary("mtimage-jni");
 6     }
 7     private ImageView iv;
 8     private Bitmap bitmap;
 9     @Override
10     protected void onCreate(Bundle savedInstanceState) {
11         super.onCreate(savedInstanceState);
12         setContentView(R.layout.activity_main);
13         
14         iv = (ImageView) findViewById(R.id.iv);
15         
16         bitmap = BitmapFactory.decodeFile("sdcard/aneiyi.jpg");
17         iv.setImageBitmap(bitmap);
18     }
19     
20     public void click(View v){
21         
22         int width = bitmap.getWidth();
23         int height = bitmap.getHeight();
24         
25         //用於儲存所有畫素資訊的陣列
26         int[] pixels = new int[width*height];
27         //獲取圖片的畫素顏色資訊,儲存至pixels
28         bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
29         
30         JNI jni = new JNI();
31         //呼叫美圖秀秀本地庫中的美圖方法,靠猜
32         //arg0:儲存了所有畫素顏色資訊的陣列
33         //arg1:圖片的寬
34         //arg2:圖片的高
35         //此方法是通過改變pixels的畫素顏色值來實現美化效果,傳遞一個數組引數是不需要返回值的
36         jni.StyleLomoB(pixels, width, height);
37         
38         Bitmap bmNew = Bitmap.createBitmap(pixels, width, height, bitmap.getConfig());
39         iv.setImageBitmap(bmNew);
40     }
41 }
複製程式碼

 

九、在C語言中呼叫java方法(反射)         1、有時需要在C語言中呼叫java的方法,如重新整理UI顯示載入資源進度            在本地方法C語言程式碼中列印 Android的Logcat日誌輸出,Google已經幫我們封裝好了方法,只需要呼叫一下就可以            如果要輸出中文的話,必須將C語言的檔案編碼改成 utf-8,否則亂碼            在C語言中呼叫java的方法需要用到反射,C語言的反射需要一個方法簽名,使用javap能夠生成方法簽名,很熟練的話也可以自己寫方法簽名            在bin/classes目錄下執行 javap -s 全類名 複製程式碼
 1 public class MainActivity extends Activity {
 2     static{
 3         System.loadLibrary("hello");
 4     }
 5     
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         super.onCreate(savedInstanceState);
 9         setContentView(R.layout.activity_main);
10     }
11     
12     public void click(View v){
13         cLog();
14     }
15     
16     public native void cLog();
17     
18     public void show(String message){
19         Builder builder = new Builder(this);
20         builder.setTitle("標題");
21         builder.setMessage(message);
22         builder.show();
23     }
24     
25 }
複製程式碼 複製程式碼
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
JNIEXPORT void JNICALL Java_com_mwp_ccalljava2_MainActivity_cLog
  (JNIEnv * env, jobject obj){
    //列印log輸出
    LOGD("我是C語言列印的debug日誌");
    LOGI("我是C語言列印的info日誌");
    //通過反射來呼叫java的方法,需要知道方法簽名,使用javap得到方法簽名
    //在bin/classes目錄下執行 javap -s 全類名
    //1、得到類的位元組碼物件
    //jclass      (*FindClass)(JNIEnv*, const char*);
    jclass clazz = (*env)->FindClass(env, "com/mwp/ccalljava2/MainActivity");
    //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID methodID = (*env)->GetMethodID(env, clazz, "show", "(Ljava/lang/String;)V");
    //void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    (*env)->CallVoidMethod(env,obj,methodID, (*env)->NewStringUTF(env, "這是彈窗的內容"));
}
複製程式碼
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS += -llog
LOCAL_MODULE    := hello
LOCAL_SRC_FILES := log.c
include $(BUILD_SHARED_LIBRARY)


十、模擬監測壓力感測器

        感測器的原理是使用敏感電阻如(光敏電阻,熱敏電阻)等監測電流電壓的變化         Android程式只需要處理感測器傳遞的資料,並將其顯示在介面上就可以。         下面模擬一個壓力感測器來練習JNI程式設計
複製程式碼
 1 public class MainActivity extends Activity {
 2     static{
 3         System.loadLibrary("monitor");
 4     }
 5     private MyProgressBar mpb;
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         super.onCreate(savedInstanceState);
 9         setContentView(R.layout.activity_main);
10         
11         mpb = (MyProgressBar) findViewById(R.id.mpb);
12         mpb.setMax(100);
13     }
14     
15     public void start(View v){
16         new Thread(){
17             public void run() {
18                 startMonitor();                
19             };
20         }.start();
21     }
22     
23     public void stop(View v){
24         stopMonitor();
25     }
26     
27     public native void startMonitor();
28     public native void stopMonitor();
29     
30     //供本地方法呼叫重新整理UI
31     public void show(int pressure){
32         mpb.setPressure(pressure);
33     }
34 }
複製程式碼 複製程式碼
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
//模擬壓力感測其傳遞資料
int getPressure(){
    return rand()%101;
}
//用於控制迴圈的開關
int monitor;
JNIEXPORT void JNICALL Java_com_mwp_monitor_MainActivity_startMonitor
  (JNIEnv * env, jobject obj){
    monitor = 1;
    int pressure;
    jclass clazz;
    jmethodID methodid;
    while(monitor){
        //本地方法獲取感測器資料
        pressure= getPressure();
        //使用反射呼叫java方法重新整理介面顯示
        //jclass      (*FindClass)(JNIEnv*, const char*);
        clazz= (*env)->FindClass(env, "com/mwp/monitor/MainActivity");
        //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
        methodid= (*env)->GetMethodID(env, clazz, "show","(I)V");
        // void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
        (*env)->CallVoidMethod(env, obj, methodid, pressure);
        sleep(1);
    }
}
JNIEXPORT void JNICALL Java_com_mwp_monitor_MainActivity_stopMonitor
(JNIEnv * env, jobject obj){
    //結束迴圈
    monitor = 0;
}
複製程式碼

 

十一、使用C++程式碼實現本地方法          1、把c檔案字尾名換成cpp          2、Android.mk檔案中的hello.c也要換成hello.cpp          3、c++的使用的環境變數結構體中,訪問了c使用的結構體的函式指標,函式名全部都是一樣的,只是引數去掉了結構體指標          4、訪問函式指標時,把env前面的*號去掉,因為此時env已經是一級指標          5、clean,清除之前編譯的殘留檔案          6、把宣告函式的h檔案放入jni資料夾中,include該h文 複製程式碼
#include <jni.h>
#include "com_mwp_cplusplus_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_mwp_cplusplus_MainActivity_helloC
  (JNIEnv * env, jobject obj){
    char* cstr = "hello from c";
  //return (*env)->NewStringUTF(env, cstr);
    return env->NewStringUTF(cstr);
}
複製程式碼

 

 

 

 

 





<本文轉自:http://www.cnblogs.com/rocomp/p/4892866.html>