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,不FQ可以在这里下载。如果可以FQ,那么就去这里。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类型。

    基本上这个模板就非常地清晰了。我们之后就可以基于这个模版来写了。至于更多的知识,以后用到了再学习了。当然,运行结果也是我们所需要的。


原文地址:https://www.cnblogs.com/wuyida/p/6299950.html