Android NDK开发介绍

android studio 教程 | 2018-09-03 14:50

本篇介绍下NDK开发,NDK就是在java中调用c/c++代码,也包括c/c++中调用java代码,这块主要就是反射机制。本篇从概念上介绍了下NDK,什么场景下需要NDK,并且举了两个例子看看NDK的实际应用。并且介绍了下NDK调试的大致思想。

NDK(The Native Development Kit)是一套工具,可以让你在Android中使用C/C++代码。直接一点就是NDK中包含了一些native的库,我们在写native的二进制时经常用到,比如libm,libc,liblog,libandroid,libcamera2ndk等,如果在android app中有一个功能需要使用这个库,怎么办?那就使用ndk。ndk主要用于下面场景中:

再介绍几个相关的名词:交叉编译:在NDK开发中,涉及的平台的比较多,比如常见的arm,x86,就需要在一个平台上编译另一个平台运行的代码JNI: Java Native Interface是java平台的一部分,允许java代码和其他语言写的代码进行交互。JNI实现的流程可以参考下这个图

首先先在AS中配置下ndk开发环境,可以自己下载ndk,也可以用SDK manager,下载好后,在project structure中指定ndk路径。这样就NDK开发环境配置好了。先来写一个例子。例子就是在Java中调用c的代码,并且在c代码中打印一条日志。在AS中新建工程,选择添加c++支持,这样新建好工程就发现里面已经有一个例子了,而我们需要做的就是在c的实现代码中打印一句log。在介绍如何实现以前,需要了解下c的编译。有两种编译c代码的方法,一种是ndk-build,一种是CMake。ndk-build是基于Android.mk文件,而CMake是基于CMakeLists.txt.AS中默认是使用CMake。至于ndk-build和CMake的区别,在AS中,默认的就是CMake,因为在编译跨平台代码时比较容易,而ndk-build完全就是因为有很多历史代码还在用它编译,所以没有去掉,可能以后AS就会完全只用CMake了,所以对于我们,还是直接用CMake吧。参考下google的原话.

Android Studio’s default build tool to compile native libraries is CMake. Android Studio also supports ndk-build due to the large number of existing projects that use the build toolkit. However, if you are creating a new native library, you should use CMake.

那我们就直接用CMake。大致介绍下ndk-build,就是在Android.mk中配置好编译文件,编译目标,编译就行,比较简单。CMake中,我们看到默认的CMakeLists.txt中已经链接了liblog.

find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log ) target_link_libraries( # Specifies the target library.                       native-lib                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )那接下来我们做的就是直接修改代码了:

#include <jni.h> #include <string> #include <android/log.h> extern "C" JNIEXPORT jstring #define ALOGI(...) {(void)__android_log_print(ANDROID_LOG_INFO, "TEST", __VA_ARGS__);} JNICALL Java_com_example_fox_ndktest_MainActivity_stringFromJNI(        JNIEnv *env,        jobject /* this */) {    std::string hello = "Hello from C++";    ALOGI("native log");    return env->NewStringUTF(hello.c_str()); }这里面只有log.h,ALOGI定义,ALOGI调用是我自己加的,其余都是已经有的。这样就已经好了,直接运行代码,就会看到log可以打印出来了:

09-02 17:13:46.162 18019-18019/com.example.fox.ndktest D/HwGalleryCacheManagerImpl: mIsEffect:false 09-02 17:13:46.168 18019-18019/com.example.fox.ndktest D/HwRTBlurUtils: check blur style for HwPhoneWindow, themeResId : 0x7f0c0005, context : com.example.fox.ndktest.MainActivity@699c1ce, Nhwext : 0, get Blur : disable with , null 09-02 17:13:46.169 18019-18019/com.example.fox.ndktest D/HwRTBlurUtils: check blur style for HwPhoneWindow, themeResId : 0x7f0c0005, context : com.example.fox.ndktest.MainActivity@699c1ce, Nhwext : 0, get Blur : disable with , null 09-02 17:13:46.197 18019-18019/com.example.fox.ndktest I/TEST: native log 09-02 17:13:46.199 18019-18019/com.example.fox.ndktest D/ActivityThread: add activity client record, r= ActivityRecord{bc9f6fb token=android.os.BinderProxy@de0693 {com.example.fox.ndktest/com.example.fox.ndktest.MainActivity}} token= android.os.BinderProxy@de0693 09-02 17:13:46.213 18019-18019/com.example.fox.ndktest D/OpenGLRenderer:   HWUI Binary is  enabled 09-02 17:13:46.217 18019-18041/com.example.fox.ndktest D/OpenGLRenderer: HWUI GL Pipeline 09-02 17:13:46.225 18019-18019/com.example.fox.ndktest I/ActivityManager_activity: Resuming ActivityRecord{bc9f6fb token=android.os.BinderProxy@de0693 {com.example.fox.ndktest/com.example.fox.ndktest.MainActivity}} with isForward=true,forwardBitChanged=false onlyLocalRequest=false 09-02 17:13:46.228 18019-18019/com.example.fox.ndktest I/PressGestureDetector: enabledInPad =说明我们修改生效了。看下具体细节:

public class MainActivity extends AppCompatActivity {    // Used to load the 'native-lib' library on application startup.    static {        System.loadLibrary("native-lib");    }    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Example of a call to a native method        TextView tv = (TextView) findViewById(R.id.sample_text);        tv.setText(stringFromJNI());    }    /**     * A native method that is implemented by the 'native-lib' native library,     * which is packaged with this application.     */    public native String stringFromJNI(); }在java中先声明一个native方法stringFromJNI,native表示这个函数是用其他语言实现的。这个是自动生成的,我们也可以自己添加其他的方法。然后在static域中load 含有stringFromJNI实现的库。现在stringFromJNI的实现就是在libnative-lib.so中。 再看下CMakeLists.txt:

# For more information about using CMake with Android Studio, read the # documentation: -native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. add_library( # Sets the name of the library.             native-lib             # Sets the library as a shared library.             SHARED             # Provides a relative path to your source file(s).             src/main/cpp/native-lib.cpp ) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library.                       native-lib                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )这儿的注释已经介绍的很清楚了,如果想添加一个已经编译好的三方库,可以参考下

add_library( imported-lib             SHARED             IMPORTED ) set_target_properties( # Specifies the target library.                       imported-lib                       # Specifies the parameter you want to define.                       PROPERTIES IMPORTED_LOCATION                       # Provides the path to the library you want to import.                       imported-lib/src/${ANDROID_ABI}/libimported-lib.so )IMPORTED 关键字表示这个库已经生成了,不需要编译了,导入就行。下面的set_target_properties是告诉CMake去哪儿找import的库。这儿可以用工具看下编译好的apk中是否包含了native-lib库,点下build中的Analyze APK,可以看到libnative-lib.so已经在apk中了。

在C中可以调用Java方法,因为Java中有重载机制,因此在查找需要调用的方法时就需要一套机制可以找到对应的方法,这就涉及到了函数的签名。每个Java类型在JNI中都有对应的表示,看下表

也可以使用命令javap -s 包名.类名 来打印方法签名,前提是需要在build目录下执行改名了才行,否则会找不到class文件。在Java中添加一个静态方法,这里是callLogMessage:

public class MainActivity extends AppCompatActivity {    // Used to load the 'native-lib' library on application startup.    static {        System.loadLibrary("native-lib");    }    public static void logMessage(String data) {        Log.d("TEST", data);    }    public static void callLogMessage(String data) {        logMessage(data);    }    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Example of a call to a native method        TextView tv = (TextView) findViewById(R.id.sample_text);        tv.setText(stringFromJNI());    }    /**     * A native method that is implemented by the 'native-lib' native library,     * which is packaged with this application.     */    public native String stringFromJNI(); }然后修改下native的实现:

JNICALL Java_com_example_fox_ndktest_MainActivity_stringFromJNI(        JNIEnv *env,        jobject /* this */) {    std::string hello = "Hello from C++";    ALOGI("native log");    jclass clsLog = env->FindClass("com/example/fox/ndktest/MainActivity");    if(clsLog == NULL) {        ALOGI("Find MainActivity fail");        return env->NewStringUTF(hello.c_str());    }    jmethodID mthStaticMethod = env->GetStaticMethodID(clsLog,"callLogMessage", "(Ljava/lang/String;)V");    if (NULL == mthStaticMethod) {        ALOGI("Find Method fail");        return env->NewStringUTF(hello.c_str());    }    jstring data =env->NewStringUTF("give by native method");    env->CallStaticVoidMethod(clsLog,mthStaticMethod,data);    env->DeleteLocalRef(clsLog);    env->DeleteLocalRef(data);    return env->NewStringUTF(hello.c_str()); }这儿的逻辑就是java调用一个方法,该方法用c实现,在实现中又调用了java的方法,并将一个字符串传给java的方法,java方法中将该字符串打印出来。编译运行下:

09-02 19:10:51.990 25240-25240/com.example.fox.ndktest D/HwGalleryCacheManagerImpl: mIsEffect:false 09-02 19:10:51.997 25240-25240/com.example.fox.ndktest D/HwRTBlurUtils: check blur style for HwPhoneWindow, themeResId : 0x7f0c0005, context : com.example.fox.ndktest.MainActivity@699c1ce, Nhwext : 0, get Blur : disable with , null 09-02 19:10:51.998 25240-25240/com.example.fox.ndktest D/HwRTBlurUtils: check blur style for HwPhoneWindow, themeResId : 0x7f0c0005, context : com.example.fox.ndktest.MainActivity@699c1ce, Nhwext : 0, get Blur : disable with , null 09-02 19:10:52.028 25240-25240/com.example.fox.ndktest I/TEST: native log 09-02 19:10:52.028 25240-25240/com.example.fox.ndktest D/TEST: give by native method 09-02 19:10:52.034 25240-25240/com.example.fox.ndktest D/ActivityThread: add activity client record, r= ActivityRecord{bc9f6fb token=android.os.BinderProxy@de0693 {com.example.fox.ndktest/com.example.fox.ndktest.MainActivity}} token= android.os.BinderProxy@de0693 09-02 19:10:52.050 25240-25240/com.example.fox.ndktest D/OpenGLRenderer:   HWUI Binary is  enabled 09-02 19:10:52.052 25240-25283/com.example.fox.ndktest D/OpenGLRenderer: HWUI GL Pipeline可以看到log已经可以打印出来了,证明修改生效了。 在jni中可以调用Java的方法,包括静态和非静态的,也可以修改java的变量,方法相似,这儿就不展示了,具体机制和java spring的IOC比较像。

NDK出现问题后,比较麻烦。java中出问题可以通过调用栈找到出问题的地方,NDK的话,现象就是app闪退,这时候定位信息比较少,这时候就只能针对ndk的lib库进行分析了,这块可以用上native二进制的分析方法,一些命令是必不可少的,比如nm,addr2line,objdump,readelf,ndk-statck等。我在刚开始写NDK时,出现过命名方法已经实现了,可是应用还是提示闪退,找不到对应的实现方法,用nm看了下变异的lib库,发现是native方法的修饰错了,我用了c++的修饰方法,导致函数名不一样找不到实现,用extern “C” 标注下就解决了。如果需要用addr2line,objdump等命令时,记得用带符号表的so,而不是最终运行的so,在AS中会编译出来带符号的so,这里面有很全的信息,比如变量名字和行号的对应等。

本篇介绍了下NDK的相关问题,NDK在开发游戏等计算比较大的场景中用的比较多,因此还是有必要学习下的。