1. 程式人生 > >Android開發學習之路--NDK、JNI之初體驗

Android開發學習之路--NDK、JNI之初體驗

    好久沒有更新部落格了,最近一直在看一個仿微信專案,然後看原始碼並自己實現下,相信經過這個專案可以讓自己瞭解一個專案中的程式碼以及種種需要注意的事項。不知不覺中部落格已經快要40w訪問量,而且排名也即將突破3000了,在此感謝朋友們的支援和認可。今天趁著有點時間就來完成早就想要完成的jni技術了。

    說到jni可能初學者會不知道,其實就是java native interface,也就是java程式碼需要呼叫底層的c/c++程式碼,那麼就需要通過jni來實現了,android手機的底層是linux,而linux之上跑的一般都是c/c++程式碼,而我們app是java程式碼,雖然一般情況下開發app是不需要了解jni的,但是有些需要高效率的事情,比如音視訊編解碼,比如3d繪圖等就需要用c/c++來實現了,而且這些演算法在c/c++上都是非常成熟的。講了這麼多,這裡還是簡單地來實現下了。

    記得以前在windows下用cgwin來編譯ndk很不舒服,現在用mac了,用android studio,那就在這個環境下來簡單實現一個測試例子了,android studio確實方便。首先我們需要下載ndk,http://www.androiddevtools.cn,不翻牆可以在這裡下載。如果可以翻牆,那麼就去這裡。http://developer.android.com/intl/zh-cn/ndk/downloads/index.html。下載好後,放到自己想要放的目錄下,然後執行如下命令:

chmod a+x android-ndk-r10e-darwin-x86_64.bin
./android-ndk-r10e-darwin-x86_64.bin
    以上命令其實就是把下載好的包解壓縮出來。最後會生產一個android-ndk-r10的資料夾,裡面就是一系列ndk需要用的東西了。
    既然已經下載好了ndk,那麼接下來就來測試下了。首先是新建工程emJniStudy。編寫ndkjniutils,程式碼如下:
package com.jared.emjnistudy;

/**
 * Created by jared on 16/2/28.
 */
public class NdkJniUtils {
    static {
        System.loadLibrary("emJniLibName");	//defaultConfig.ndk.moduleName
    }

    public native String getCLanguageString();
}

    這裡的loadLibrary主要是載入.so檔案,一般linux下的庫檔案都是.so結尾的。這裡的庫名字是emJniLibName。這裡後面再瞭解怎麼定義這個名字的。然後有一個native開頭表示是jni的介面。這裡的函式是獲取c的string,也就是c程式碼中的一串字串了。

    接著我們來根據這個java程式碼實現c程式碼的標頭檔案,這裡先build下我們的程式碼,需要有一個class檔案才行。

    進入當前工程目錄:

cd app/build/intermediates/classes/debug

    然後通過命令列:

javah -jni com.jared.emjnistudy.NdkJniUtils

其中com.jared.emjnistudy是包名,NdkJniUtils是java程式碼。然後會在當前目錄下生成

com_jared_emjnistudy_NdkJniUtils.h

開啟檔案可以看到原始碼如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jared_emjnistudy_NdkJniUtils */

#ifndef _Included_com_jared_emjnistudy_NdkJniUtils
#define _Included_com_jared_emjnistudy_NdkJniUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jared_emjnistudy_NdkJniUtils
 * Method:    getCLanguageString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jared_emjnistudy_NdkJniUtils_getCLanguageString
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

    這裡就會生產一個需要c實現的函式介面了。接著在main目錄下新建jni目錄,然後把這個標頭檔案拷貝到jni目錄下,然後新建一個c檔案,命名為jnitest.c。編寫jnitest.c如下:
#include "com_jared_emjnistudy_NdkJniUtils.h"

JNIEXPORT jstring JNICALL Java_com_jared_emjnistudy_NdkJniUtils_getCLanguageString
        (JNIEnv *env, jobject obj) {
    return (*env)->NewStringUTF(env, "This is Jni test!!!");
}

    這裡只要實現標頭檔案函式即可,也就是return一串字串。這樣庫檔案和jni介面都準備好了,接著呢我們需要來配置下編譯的gradle了。首先是:
vi gradle.properties
配置檔案最後新增一行程式碼如下:
android.useDeprecatedNdk=true

接著修改app目錄下的build.gradle:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.jared.emjnistudy"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        ndk {
            moduleName "emJniLibName"			         //生成的so名字
            abiFilters "armeabi", "armeabi-v7a", "x86"	//輸出指定三種abi體系結構下的so庫。目前可有可無。
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
}

    這裡在defaultConfig中添加了ndk。其中moduleName就是上面java程式碼中load的名字,emJniLibName。這裡制定了三種abi體系結構下的so庫,所謂體系結構就是linux下需要編譯不同晶片需要不同的交叉編譯工具鏈。因為不同的晶片,比如是pc,那麼需要用gcc編譯才可以在pc上跑程式,如果是arm的就需要用arm提供的交叉編譯工具才可以跑。

    一切準備就緒,那麼最後我們在Activity中需要顯示c程式碼中的這句字串,修改layout程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.jared.emjnistudy.MainActivity">

    <TextView
        android:id="@+id/hello"
        android:text="Hello World!"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

    修改MainActivity程式碼:
package com.jared.emjnistudy;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

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

        helloJni = (TextView)findViewById(R.id.hello);
        NdkJniUtils jniUtils = new NdkJniUtils();
        String text = jniUtils.getCLanguageString();
        helloJni.setText(text);
    }
}

    然後我們執行下程式碼看下效果如下:


    從上圖可以看到我們需要的內容,ndk編譯,jni實現ok了。之後的話很多東西都可以在c中來完成了。是不是很簡單,比起以前的cgwin不知道要方便多少。這裡看下jni的c程式碼到底是怎麼編譯的呢? 進入目錄

cd app/build/intermediates/ndk/debug

    接著我們我們看下一個Android.mk。如果看過android原始碼就會看過很多的Android.mk,其實這個就類似於linux下的Makefile,也就是編譯程式碼用的,就是編譯.so庫的指令碼。看下程式碼如下:
LOCAL_PATH := $(call my-dir)
   include $(CLEAR_VARS)
   
   LOCAL_MODULE := emJniLibName
   LOCAL_LDFLAGS := -Wl,--build-id
   LOCAL_SRC_FILES := \
       /Users/jared/Documents/workspace/android/emJniStudy/app/src/main/jni/jnitest.c \
   
   LOCAL_C_INCLUDES += /Users/jared/Documents/workspace/android/emJniStudy/app/src/main/jni
   LOCAL_C_INCLUDES += /Users/jared/Documents/workspace/android/emJniStudy/app/src/debug/jni
  
   include $(BUILD_SHARED_LIBRARY)

 這裡編譯後的庫名字就是emJniLibName,需要進行編譯的原始碼是jnitest.c了。

   然後我們看下編譯生產的project下的.so檔案。


    logcat列印資訊如下,已經載入成功了。

02-28 00:36:30.220 1266-1266/com.jared.emjnistudy D/dalvikvm: Trying to load lib /data/app-lib/com.jared.emjnistudy-1/libemJniLibName.so 0xb1da7598
02-28 00:36:30.220 1266-1266/com.jared.emjnistudy D/dalvikvm: Added shared lib /data/app-lib/com.jared.emjnistudy-1/libemJniLibName.so 0xb1da7598
02-28 
    基本上jni的簡單使用已經ok了。接著我們繼續來實現個a+b。修改NdkJniUtils程式碼如下:
package com.jared.emjnistudy;

/**
 * Created by jared on 16/2/28.
 */
public class NdkJniUtils {
    static {
        System.loadLibrary("emJniLibName");	//defaultConfig.ndk.moduleName
    }

    public native String getCLanguageString();

    public native int calAAndB(int a, int b);
}
    這裡添加了calAAndB方法。然後重新生成標頭檔案。如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jared_emjnistudy_NdkJniUtils */

#ifndef _Included_com_jared_emjnistudy_NdkJniUtils
#define _Included_com_jared_emjnistudy_NdkJniUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jared_emjnistudy_NdkJniUtils
 * Method:    getCLanguageString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jared_emjnistudy_NdkJniUtils_getCLanguageString
  (JNIEnv *, jobject);

/*
 * Class:     com_jared_emjnistudy_NdkJniUtils
 * Method:    calAAndB
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_jared_emjnistudy_NdkJniUtils_calAAndB
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

   接著修改c程式碼如下:
#include "com_jared_emjnistudy_NdkJniUtils.h"

JNIEXPORT jstring JNICALL Java_com_jared_emjnistudy_NdkJniUtils_getCLanguageString
        (JNIEnv *env, jobject obj) {
    return (*env)->NewStringUTF(env, "This is Jni test!!!");
}

JNIEXPORT jint JNICALL Java_com_jared_emjnistudy_NdkJniUtils_calAAndB
        (JNIEnv *env, jobject obj, jint a, jint b) {
    return (a+b);
}

    最後我們在MainActivity中新增一個程式碼:
package com.jared.emjnistudy;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

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

        helloJni = (TextView)findViewById(R.id.hello);
        NdkJniUtils jniUtils = new NdkJniUtils();
        String text = jniUtils.getCLanguageString();
        helloJni.setText(text);
        
        String res = String.valueOf(jniUtils.calAAndB(10, 30));
        Toast.makeText(getApplicationContext(), res, Toast.LENGTH_LONG).show();
    }
}

    執行看下效果:     很明顯10+30等於40,最後返回了我們要的結果。     既然已經會了簡單的jni呼叫了,但是發現了一個問題,No JNI_OnLoad found。貌似少了onload函式,記得以前研究android原始碼的時候,onload函式是需要的,還有一大推函式呼叫的函式指標,還有註冊函式。
02-28 02:58:38.366 20431-20431/com.jared.emjnistudy D/dalvikvm: No JNI_OnLoad found in /data/app-lib/com.jared.emjnistudy-2/libemJniLibName.so 0xb1d9fc90, skipping init
    那麼接下來我們就來小研究下,簡單地實現下這個小模版了。修改.h和.c程式碼如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jared_emjnistudy_NdkJniUtils */

#ifndef _Included_com_jared_emjnistudy_NdkJniUtils
#define _Included_com_jared_emjnistudy_NdkJniUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jared_emjnistudy_NdkJniUtils
 * Method:    getCLanguageString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL getCLanguageString
  (JNIEnv *, jobject);

/*
 * Class:     com_jared_emjnistudy_NdkJniUtils
 * Method:    calAAndB
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL calAAndB
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

    這裡把函式名都簡化了,待會兒就知道為什麼了,接著是.c程式碼:
#include <string.h>
#include <assert.h>
#include "com_jared_emjnistudy_NdkJniUtils.h"

JNIEXPORT jstring JNICALL getCLanguageString
        (JNIEnv *env, jobject obj) {
    return (*env)->NewStringUTF(env, "This is Jni test!!!");
}

JNIEXPORT jint JNICALL calAAndB
        (JNIEnv *env, jobject obj, jint a, jint b) {
    return (a+b);
}

//引數對映表
static JNINativeMethod methods[] = {
        {"getCLanguageString", "()Ljava/lang/String;", (void*)getCLanguageString},
        {"calAAndB", "(II)I", (void*)calAAndB},
};

//自定義函式,為某一個類註冊本地方法,調運JNI註冊方法
static int registerNativeMethods(JNIEnv* env , const char* className , JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }

    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNatives(JNIEnv* env)
{
    const char* kClassName = "com/jared/emjnistudy/NdkJniUtils";//指定要註冊的類
    return registerNativeMethods(env, kClassName, methods,  sizeof(methods) / sizeof(methods[0]));
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    //動態註冊,自定義函式
    if (!registerNatives(env)) {
        return -1;
    }

    return JNI_VERSION_1_4;
}

    這裡可以發現JNI_Onload函式,當啟動程式的時候,會載入庫檔案,就會呼叫這個函式,之前簡單的例子沒有這個函式,所以就有log列印資訊了。接著在onload函式中,我們註冊了nativemethods。這裡註冊了剛剛實現的兩個方法。

    可以看出methods陣列中第一個和第三個引數比較好理解,那麼第二個引數呢?其實第二個引數可以參考標頭檔案,一模一樣拉過來就好了。其中的意思就是()裡的表示函式的引數,()表示沒有引數,(II)表示兩個引數,都是int。後面跟的Ljava/lang/String表示返回值是String型別的,I表示的是int型別。

    基本上這個模板就非常地清晰了。我們之後就可以基於這個模版來寫了。至於更多的知識,以後用到了再學習了。當然,執行結果也是我們所需要的。