Android NDK开发总结

一、准备Android NDK开发环境

NDK:android原生开发工具包,这套工具集允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,让您可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。

CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。

LLDB:一种调试程序,Android Studio 使用它来调试原生代码

 

可以从 SDK 管理器中安装 LLDB、CMake 和 NDK

1.在打开的项目中,从菜单栏选择 Tools > Android > SDK Manager

2.点击 SDK Tools 标签。

3.选中 LLDBCMake 和 NDK 旁的复选框,如图所示。

4.点击 Apply,然后在弹出式对话框中点击 OK。

5.安装完成后,点击 Finish,然后点击 OK。

Android NDK开发总结

本片文章使用ndk-build来构建ndk项目,cmake方式本篇不做阐述。

二、要了解的知识点

Android.mk:

位于项目JNI目录的子目录中,用于向编译系统描述源文件和共享库。它实际上是编译系统解析一次或多次的微小 GNU makefile 片段。Android.mk文件用于定义Application.mk、编译系统和环境变量所未定义的项目范围设置。它还可替换特定模块的项目范围设置。

内容解析:

LOCAL_PATH := $(call my-dir)//编译系统提供宏函数 my-dir 返回当前目录(Android.mk文件本身所在的目录)的路径。
include $(CLEAR_VARS)//CLEAR_VARS变量指向一个特殊的 GNU Makefile,后者会清除许多 LOCAL_XXX 变量,例如 LOCAL_MODULELOCAL_SRC_FILES 和 LOCAL_STATIC_LIBRARIES,但是不会清除不会清除 LOCAL_PATH。在描述每个模块之前,必须声明(重新声明)此变量
LOCAL_MODULE := hello-jni//每个模块名称必须唯一,且不含任何空格。编译系统在生成最终共享库文件时,会对您分配给 LOCAL_MODULE 的名称自动添加正确的前缀和后缀。例如,上述示例会生成名为 libhello-jni.so 的库
LOCAL_SRC_FILES := hello-jni.c//列举要编译到模块中的C、C++源文件,以空格分隔多个文件
LOCAL_CPP_EXTENSION := .cxx //使用此可选变量为 C++源文件指明.cpp以外的文件扩展名。例如.cxx
LOCAL_SHARED_LIBRARIES := libz //此变量会列出此模块在运行时依赖的共享库模块,如libz.so。此信息是链接时必需的信息,用于将相应的信息嵌入到生成的文件中
LOCAL_STATIC_LIBRARIES := libz //此变量用于存储当前模块依赖的静态库模块列表,如libz.a。此信息是链接时必需的信息,用于将相应的信息嵌入到生成的文件中(需要先预编译libz.a才能找到)
LOCAL_LDLIBS := -lz //此变量列出了在编译共享库或可执行文件时使用的额外链接器标记,可使用-l前缀传递特定系统库的名称,如-lz表示libz.so。

LOCAL_CFLAGS //只编译 C++ 源文件时将传递的一组可选编译器标记。它们将出现在编译器命令行中的 LOCAL_CFLAGS 后面。使用 LOCAL_CFLAGS 为 C 和 C++ 指定标记。比如:= -frtti -fexceptions --std=c++11 -DANDROID -DNDEBUG
LOCAL_C_INCLUDES := sources/foo //可以使用此可选变量指定相对于 NDK root 目录的路径列表,以便在编译所有源文件(C、C++ 和 Assembly)时添加到 include 搜索路径
include $(BUILD_SHARED_LIBRARY)//将所有内容连接到一起
include $(PREBUILT_SHARED_LIBRARY) //指向用于指定预编译共享库的编译脚本
include $(PREBUILT_STATIC_LIBRARY)//预编译静态库

LOCAL_ARM_MODE := arm //默认情况下,编译系统会在 thumb 模式下生成 ARM 目标二进制文件,其中每条指令都是 16 位宽,并与 thumb/ 目录中的 STL 库关联。将此变量定义为 arm 会强制编译系统在 32 位 arm 模式下生成模块的对象文件
 
Application.mk
位于项目JNI目录的子目录中,用于指定项目范围设置。(在Android.mk中设置的模块选项优先于项目范围选项)
内容解析:
APP_OPTIM := release //将此可选变量定义为 release 或 debug。默认情况下,将编译发布模式的二进制文件,发布模式会启用优化。
APP_ABI := armeabi-v7a arm64-v8a x86 //默认情况下,NDK 编译系统会为所有非弃用 ABI 生成代码。您可以使用 APP_ABI 设置为特定 ABI 生成代码

Android NDK开发总结

APP_PLATFORM := android-16 //声明编译此应用所面向的 Android API 级别,并对应于应用的 minSdkVersion
APP_STL :=  c++_static //用于此应用的 C++ 标准库。默认情况下使用 system STL。其他选项包括 c++_sharedc++_static 和 none 

java与jni数据类型对照以及使用
1.综述:

1)java中的返回值void与jni中的void是完全对应的。

2)java中的基本数据类型(byte,short,int,long,float,double,boolean,char)在jni中对应的数据类型在前面加上j (jbyte,jshort,jint,jlong,jfloat,jdouble,jboolean,jchar)。

3)java中的对象,包括类库中定义的类、接口,都对应jni中的jobject。

4)java中基本数据类型的数组对应与jni中的jarray类型(type就是上面说的8中基本数据类型

5)java中对象的数组对应于jni中jobjectArray类型

 

2.java数据类型与jni类型映射表

Android NDK开发总结

3.java类型和jni签名的关系

Android NDK开发总结

 

4.常用的函数:

jclass      (*FindClass)(JNIEnv*, const char*);

jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);

jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);

jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);

void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);

jsize       (*GetStringLength)(JNIEnv*, jstring);

......

三、自定义Android Studio工具

1.在打开的项目中,从菜单栏选择 Tools > Android > SDK Manager

2.点击 Tools > External Tools

Android NDK开发总结

3.点击OK保存。这时候就可以在菜单栏中看到javah选项,后续就可以直接右键生成java文件对应的C++头文件了。

 Android NDK开发总结

四、简单的Demo

1.创建Android 工程,在java同级目录下创建jni目录,并创建Android.mk和Application.mk文件

2.在Android.mk中将原生库添加到 LOCAL_LDLIBS变量中,例如要链接liblog.so,添加代码如下:

LOCAL_LDLIBS := -llog; //编译系统会自动链接标准 C 库和 C++ 库,无需在这里添加

使用空格作为分隔符列出多个原生库。

3.在Java层创建接口文件HelloWorld.java

public class HelloWorld {
    public native String ToastHelloFromC(String javaStr);
}

4.右键javah,生成对应的JNI头文件

JNIEXPORT jstring JNICALL Java_com_axu_ndkdemo_HelloWorld_ToastHelloFromC
  (JNIEnv *, jobject, jstring);

5.根据上一步生成的头文件生成C++源文件并编写逻辑

jstring JNICALL Java_com_axu_ndkdemo_HelloWorld_ToastHelloFromC(JNIEnv * env, jobject jobj, jstring javaStr)
{
    const char * javaChar_ptr = env -> GetStringUTFChars(javaStr,NULL);
    char jniChar[] = "and 我来自JNI";

    char* result_ptr = (char*)malloc(strlen(javaChar_ptr) + strlen(jniChar) + 1);
    strcpy(result_ptr, javaChar_ptr);
    strcat(result_ptr, jniChar);
    return env -> NewStringUTF(result_ptr);
}

Android NDK开发总结

6.完善Android.mk和Application.mk

Android.mk:

LOCAL_PATH := $(call my-dir)


######## build hello.so start #########
include $(CLEAR_VARS)

LOCAL_CPP_EXTENSION := .cpp

LOCAL_MODULE := hello

LOCAL_SRC_FILES := com_axu_ndkdemo_HelloWorld.cpp
#LOCAL_C_INCLUDES += com_axu_ndkdemo_HelloWorld.h

LOCAL_CPPFLAGS:= -frtti -fexceptions --std=c++11 -DANDROID -DNDEBUG

include $(BUILD_SHARED_LIBRARY)

######### build hello.so end ##########

Application.mk:

#APP_OPTIM := release

APP_ABI := armeabi-v7a arm64-v8a x86 x86_64

APP_PLATFORM := android-16

#APP_STL := c++_static

7.在jni目录执行ndk-build命令,生成对应CPU架构的so文件

Android NDK开发总结

8.配置app gradle 将so加载进去,并编写调用程序

sourceSets {
        main {
            jniLibs.srcDirs 'src/main/libs'
            jni.srcDirs = []
        }
    }
Android NDK开发总结Android NDK开发总结
public class MainActivity extends Activity {
    private Button getJni_btn;
    private HelloWorld helloWorld;

    static {
        try{
            System.loadLibrary("hello");
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }


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

        init();

        getJni_btn = (Button)findViewById(R.id.get_jni_btn);
        getJni_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String result = helloWorld.ToastHelloFromC(" 我来自JAVA ");
                Log.e("MainActivity",result);
            }
        });
    }

    private void init(){
        helloWorld = new HelloWorld();
    }
}
View Code

9.运行程序,Logcat输出:

Android NDK开发总结

四、熟练掌握JNI层与Java层交互

1、Java调用C代码

这种情况一般是正向调用,应用场景为java层应用需要使用C/C++层中的方法,常见于调用设备硬件驱动,优化算法,要求计算效率等接口中。

常见的形式有数值运算,字符串运算、数组运算以及复杂对象运算,下面分别举例说明

1)数值运算

2)字符串运算

3)数组运算

4)对象运算

Android NDK开发总结

2、C代码调用Java

 这种情况可称为反向调用,最常见的应用场景为C/C++层需要将处理信息或者执行结果异步回调给java层。

 1)保存全局JVM实例和回调函数的实例对象

2)从全局jvm中获取当前线程的JNIEnv实例

3)获取回调函数的实例对象字节码jclass

4)通过GetMethodId方法获得回调函数的方法id

5)通过CallVoidMethod调用java方法


需要注意的地方:
如果需要在jni层回调函数内处理java对象,分两种情况:
1.如果该对象在回调函数中使用到(传参或返回值),这可以直接通过FindClass找到该对象的字节码
2.如果该对象在回调函数中没有被使用到,那么就需要提前注册该类型的对象,然后在回调函数中取出对象进行使用

3、复杂对象的数据互通
复杂对象指对象列表,对象中嵌套对象。原理一样,当C解析java传过来的对象时,使用FindClass找到对象然后解析字段
当C组装java对象并传给java层时,需要先找到对象的构造方法id,之后通过构造方法进行对象赋值,从而创建对象,之后直接抛到java层,java层使用相应的对象接收即可。


上一篇:c – 交叉编译:特殊交叉编译器或只是带选项-march的gcc?


下一篇:蓝凌&钉钉全新管理中台 MK-PaaS