Android JNI开发笔记

最近开发App涉及到许多图像处理操作,使用的OpenCV开源库,考虑到性能问题,决定使用JNI层开发。之前一直停留在应用层,并没有JNI层开发的经验,心里总感觉没谱,趁这次机会好好理理,权当纪录。

工具

工欲善其事,必先利其器。开发Android的话,首推Android Studio,强大的智能提示和优秀的Gradle构建系统,能最大程度提高你的生产力;另外,既然涉及到JNI层开发,还需要下载NDK工具集

在早期版本,NDK和SDK是分别打包的;最新Android Studio 1.5,NDK和SDK捆绑,可以直接在Android Studio内安装。

JNI开发概览

开发方式

最开始Android Studio并不支持JNI开发,当时开发者只能选择Eclipse。传统的JNI开发是写好C/C++,最后通过编写Makefile编译脚本,生成so文件供应用层调用。
自从Android Studio 1.3版本加入JNI开发支持后,可以结合Gradle插件进行JNI开发,具体的编译任务通过Gradle插件完成,不需要额外编写编译脚本,并且支持JNI层调试
这里,我们通过后一种方式进行说明。具体的官方示例代码可以移步Github

具体步骤

新建Android工程

Alt text

全部选择默认设置,一路Next。

Alt text

Alt text

Alt text

修改Gradle相关配置

Andriod Studio新建工程的Gradle配置是不支持JNI开发的,需要我们手动进行调整。
找到./gradle/wrapper/gradle-wrapper.properties,修改Gradle版本为2.5。目前,只有2.5版本支持JNI模块开发,可以参考Experimental Plugin User Guide

1
2
3
4
5
6
#Wed Oct 21 11:34:03 PDT 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip

修改./build.gradle文件,将依赖模块修改为gradle-experimental

1
2
3
dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.2.0'
}

修改./app/build.gradle文件,引入modelJNI配置。

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
62
63
64
65
66
67
68
69
70
71
72
73
apply plugin: 'com.android.model.application'

model {
android {
compileSdkVersion 23
buildToolsVersion = "23.0.2"

defaultConfig.with {
applicationId = "com.aaron.nativeapp"
minSdkVersion.apiLevel = 11
targetSdkVersion.apiLevel = 23
}
}

compileOptions.with {
sourceCompatibility=JavaVersion.VERSION_1_7
targetCompatibility=JavaVersion.VERSION_1_7
}

/*
* native build settings
*/
android.ndk {
moduleName = "native"
/*
* Other ndk flags configurable here are
* cppFlags += "-fno-rtti"
* cppFlags += "-fno-exceptions"
* ldLibs = ["android", "log"]
* stl = "system"
*/
}
android.buildTypes {
release {
minifyEnabled = false
proguardFiles += file('proguard-rules.txt')
}
}
android.productFlavors {
// for detailed abiFilter descriptions, refer to "Supported ABIs" @
// https://developer.android.com/ndk/guides/abis.html#sa
create("arm") {
ndk.abiFilters += "armeabi"
}
create("arm7") {
ndk.abiFilters += "armeabi-v7a"
}
create("arm8") {
ndk.abiFilters += "arm64-v8a"
}
create("x86") {
ndk.abiFilters += "x86"
}
create("x86-64") {
ndk.abiFilters += "x86_64"
}
create("mips") {
ndk.abiFilters += "mips"
}
create("mips-64") {
ndk.abiFilters += "mips64"
}
// To include all cpu architectures, leaves abiFilters empty
create("all")
}
}

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

这个有几个坑列一下:

  • Gradle版本不对
    解决办法:重新设置Gradle的安装目录。

Alt text

  • 提示Gradle DSL method not found: 'testCompile()'
    解决办法:注释测试模块。

  • 抛出Error:Cause: org.gradle.api.internal.ExtensibleDynamicObject异常
    一般是由于操作符引起的,检查下脚本内容,确保赋值操作符都是

应用层(Java层)

新建一个包含native方法的class。

1
2
3
4
5
6
7
8
9
10
package com.aaron.nativeapp;

/**
* Created by Aaron on 15/11/11.
*/
public class NativeCore {

public static native String nativeEcho(String param);

}

nativeEcho(String param)方法很简单,就是在输入参数的基础上加上Echo字符串作为结尾。例如:Hi,JNI —> Hi,JNI Echo

创建好class后,Rebuild Project将会在./app/build/intermediates/classes/all/debug/com/aaron/nativeapp目录生成相应的.class文件。

Alt text

JNI层

打开Terminal,在./app/build/intermediates/classes/all/debug目录执行命令:

1
$ javah -jni com.aaron.nativeapp.NativeCore

生成class对应的头文件com_aaron_nativeapp_NativeCore.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_aaron_nativeapp_NativeCore */

#ifndef _Included_com_aaron_nativeapp_NativeCore
#define _Included_com_aaron_nativeapp_NativeCore
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_aaron_nativeapp_NativeCore
* Method: nativeEcho
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_aaron_nativeapp_NativeCore_nativeEcho
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

.app/src/main新建一个目录jni,将头文件剪切到该目录下。

Alt text

现在可以开始着手写C代码了。代码内容很简单,读取从应用层传来的字符串,并在结尾处添加Echo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// Created by username on 15/11/11.
//

#include <string.h>
#include "com_aaron_nativeapp_NativeCore.h"

/*
* Class: com_aaron_nativeapp_NativeCore
* Method: nativeEcho
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_aaron_nativeapp_NativeCore_nativeEcho
(JNIEnv *env, jclass thiz, jstring jParam) {
const char *c_param = env->GetStringUTFChars(jParam, JNI_FALSE);

char result[100];
strcpy(result, c_param);
strcat(result, " Echo");

env->ReleaseStringUTFChars(jParam, c_param);

return env->NewStringUTF(result);
}

加载静态库

最后一步,在NativeCore中添加静态代码段,加在本地库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.aaron.nativeapp;

/**
* Created by Aaron on 15/11/11.
*/
public class NativeCore {

static {
System.loadLibrary("native");
}

public static native String nativeEcho(String param);

}

这里静态库的名称就是在./app/build.gradleandroid.ndk定义的moduleName

调用

现在,所有的准备工作都完成了,让我们来调用NativeCore看看结果。修改MainActivity代码:

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
package com.aaron.nativeapp;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

mTextView = (TextView) findViewById(R.id.tv_content);

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();

mTextView.setText(NativeCore.nativeEcho("Hi, JNI"));
}
});
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}

return super.onOptionsItemSelected(item);
}
}

编译运行。点击右下角Fab按钮,显示JNI返回结果。

JNI调试


相对传统编写.mk进行JNI开发的方式,使用gradle-experimental插件开发的另一个好处就是支持JNI调试,选择运行选项为app-native

Alt text

在JNI层中打断点,点击Debug就可以进行调试了。附一张效果图,还是挺方便的。

可能会出现unable to attach错误:

1
2
3
4
5
DEVICE SHELL COMMAND: cat /data/local/tmp/start_lldb_server.sh | run-as com.aaron.nativeapp sh -c 'cat > /data/data/com.aaron.nativeapp/lldb/bin/start_lldb_server.sh; chmod 700 /data/data/com.aaron.nativeapp/lldb/bin/start_lldb_server.sh'
Starting LLDB server: run-as com.aaron.nativeapp /data/data/com.aaron.nativeapp/lldb/bin/start_lldb_server.sh /data/data/com.aaron.nativeapp/lldb /data/data/com.aaron.nativeapp/lldb/tmp/platform-1447213097528.sock "lldb process:gdb-remote packets"
Now Launching Native Debug Session
Failed to attach native debugger: unable to attach
unable to attach

多试几次就好了,小米1S亲测可用。