一天掌握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);
}

 

 

 

 


 






轉載請註明本文地址:一天掌握Android JNI本地編程 快速入門
發佈了0 篇原創文章 · 獲贊 104 · 訪問量 78萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章