NDK开发 - JNI开发流程

  JNI是Java和C/C++建立连接的桥梁,开发者可以将关键的加密算法通过用C/C++实现以提高被反编译的难度,保护APK的数据安全。又或者通过C/C++获取一些手机数据,以此来绕过类似Xposed这类作弊工具,提高数据的准确性。
源码地址:https://github.com/gnaix92/as-ndk

  在上一篇搭建的环境下,就可以进行NDK的开发。NDK开发主要分为一下几个步骤:

  • java层入口代码编写
  • java代码编译
  • C/C++头文件生成
  • C/C++代码编写
  • 运行编译SO

0x00 java层代码编写

  新建一个NativeMethod的Class,java层代码主要负责两件事:

Load native library(SO文件)

  Load操作很简单,其中包名为build.gradle中定义的ndk名,代码如下:

1
2
3
4
5
6
7
android.ndk {
moduleName = "native" //设置库(so)文件名称
// CFlags.add("-DCUSTOM_DEFINE")
ldLibs.addAll(["log", "android", "EGL", "GLESv1_CM"])
// ldFlags.add("-L/custom/lib/path")
stl = "stlport_static"
}
1
2
3
static{
System.loadLibrary("native");
}

定义接口名

  JNI的接口名以native字符修饰,并且不需要实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.gnaix.ndk;
/**
* 名称: NativeMethod
* 描述:
*
* @author xiangqing.xue
* @date 16/3/10
*/
public class NativeMethod {
static{
System.loadLibrary("native");
}
/**
* 基础类型
* @param i
* @return
*/
public static native int getInt(int i);
/**
* string
* @param str
* @return
*/
public static native String getString(String str);
/**
* array
* @param data
* @return
*/
public static native byte[] getByteArray(Byte[] data);
/**
* 调用java对象
* @param name
* @param age
*/
public static native void invokeJobject(String name, int age);
/**
* 调用java静态方法
*/
public static native void invokeStaticFieldAndMethod();
/**
* 获取结构体
* @return
*/
public static native Person[] getPersons();
}

  到这里java层的活就干完了。

0x01 java代码编译

  在生成C/C++的头文件前需要把上面写的class编译成.class文件。Android Studio中编译很简单点一下右上角的按钮就可以了。生成的class文件在app/build/intermediates/calsses/all/debug/xx.xx.xx/xx.class

C/C++头文件生成

  JNI开发对C/C++的头文件有命名的格式要求,所以我们可以使用jdk提供的javah命令生成规定的C/C++头文件。为了方便可以先切换到app/src/main目录下。

1
cd app/src/main

  再执行javah命令。(根据实际替换需要生成的class文件)

1
javah -d jni -classpath ../../build/intermediates/classes/all/debug com.example.gnaix.ndk.NativeMethod

参数说明:

  • classpath:类搜索路径
  • d:将生成的头文件放到当前的 jni 目录下
  • o: 指定生成的头文件名称,默认以类全路径名生成(包名+类名.h)

  执行完会发现在main目录下生成了一个jni目录,里面有个com_example_gnaix_ndk_NativeMethod.h的头文件。里面有生成好的头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_gnaix_ndk_NativeMethod */
#ifndef _Included_com_example_gnaix_ndk_NativeMethod
#define _Included_com_example_gnaix_ndk_NativeMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: getInt
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_com_example_gnaix_ndk_NativeMethod_getInt
(JNIEnv *, jclass, jint);
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: getString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_gnaix_ndk_NativeMethod_getString
(JNIEnv *, jclass, jstring);
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: getByteArray
* Signature: ([Ljava/lang/Byte;)[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_example_gnaix_ndk_NativeMethod_getByteArray
(JNIEnv *, jclass, jobjectArray);
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: invokeJobject
* Signature: (Ljava/lang/String;I)V
*/
JNIEXPORT void JNICALL Java_com_example_gnaix_ndk_NativeMethod_invokeJobject
(JNIEnv *, jclass, jstring, jint);
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: invokeStaticFieldAndMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_gnaix_ndk_NativeMethod_invokeStaticFieldAndMethod
(JNIEnv *, jclass);
/*
* Class: com_example_gnaix_ndk_NativeMethod
* Method: getPersons
* Signature: ()[Lcom/example/gnaix/ndk/Person;
*/
JNIEXPORT jobjectArray JNICALL Java_com_example_gnaix_ndk_NativeMethod_getPersons
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

0x02 C/C++代码编写

  生成头文件后就可以写具体的C++的实现代码,这里只写了一小段实现代码,具体的调用开发细节下一篇再说。 实现了getInt方法,获取了android的serialno,并用log打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//
// Created by 薛祥清 on 16/3/10.
//
#include "com_example_gnaix_ndk_NativeMethod.h"
#include <android/log.h>
#include <sys/system_properties.h>
using namespace std;
#define ENABLE_DEBUG 1
#if ENABLE_DEBUG
#define TAG "NDK_NATIVE"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG,TAG, fmt,# #args)
#define DEBUG_PRINT(format, args...) \
LOGD(format,# #args)
#else
#define DEBUG_PRINT(format, args...)
#endif
JNIEXPORT jint JNICALL Java_com_example_gnaix_ndk_NativeMethod_getInt
(JNIEnv *env, jclass object, jint num)
{
int len;
char buf[1024];
__system_property_get("ro.serialno", buf);
LOGD("name : %s", buf);
return num;
}

0x03运行编译SO

  经过上面的步骤,已经可以编译了,在NDK开发中除了自己apk需要,更多时候是作为第三方包提供给客户使用。Android Studio也为我们打包好了,可以在module目录下找到。

PS:
  • 这里需要注意debug模式编译会产生gdb.steup gdbserver这两个是gdb调试工具,在正式发布必须要用release模式编译。
  • 旧版本android-ndk-r9d只能编译armeabi armeabi-v7a mips x86四种cpu架构的,现在大多数手机已经是64位架构了,旧版本已经不能支持所有手机了。所以在android-ndk-r10e中增加了arm64-v8a mips64 x86_64三种架构。
  • 目前最新的NDK版本为android-ndk-r11b