查看原文
其他

Android JNI 篇 - 从入门到放弃

Overried 郭霖 2019-04-29



今日科技快讯


最近,苹果夺回美国上市公司市值最高公司桂冠,而微软和亚马逊在此前一段时间的市值曾超过苹果。苹果股价小幅上涨0.03%,其市值达到8215亿美元,位列美国上市公司市值榜首。而微软股价下跌1.11%,其市值降至8134亿美元,位列美国上市公司市值第二高公司;亚马逊股价下跌1.12%,其市值跌至8057亿美元,位列美国上市公司市值第三高公司。


作者简介


Everybody新年好,今年是年后第一天上班,公众号更新如约回归,希望在新的一年里我们都可以继续努力学习,共同进步!

本篇来自 Overried 的投稿文章。文章讲解Android JNI 篇 - 从入门到放弃知识进行了不错的讲解,希望对大家有所帮助。

Overried 的博客地址:

https://www.jianshu.com/u/75711cf32043


正文


JNI 涉及的名词概念

  • JNI:Java Native Interface

它是Java平台的一个特性(并不是Android系统特有的)。实现Java代码调用C/C++的代码,C/C++的代码也可以调用Java的代码.

  • 二进制库分类 : 静态库,动态库
静态库

这么解释:

.a 静态库就是好多个 .o 合并到一块的集合,经常在编译C 库的时候会看到很多.o,这个.o 就是目标文件 由 .c + .h 编译出来的。

.c 相当于 .java, .h 是 C 库对外开放的接口声明。对外开放的接口 .h 和 .c 需要一一对应,如果没有一一对应,外部模块调用了接口,编译的时候会提示找不到方法。

.a 存在的意义可以看成 Android aar 存在的意义,方便代码不用重复编译, 最终为了生成 .so (apk)

动态库

Android 环境下就是 .so ,可以直接被java 代码调用的库.

  • CPU 架构(ABI)

armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64

各个平台架构的区别就是指令集不一样,浮点运算能力不一样,按照上面排列的顺序,浮点运算能力运行从低到高。

  • armeabi:这是相当老旧的一个版本,缺少对浮点数计算的硬件支持,在需要大量计算时有性能瓶颈 (微信)

  • armeabi-v7a: ARM v7 目前主流版本,兼容 armeabi (facebook app)

  • arm64-v8a: 64 位支持 兼容 armeabi-v7a armeabi

  • mips/mips64: 极少用于手机可以忽略

  • x86/x86_64: x86 架构一般用于 TV 电视机 ,兼容 armeabi

建议 android apk 为了减少包体大小只接入 armeabi-v7a 即可

  • Android 特有的文件 :Android.mk Application.mk
  • Android.mk:在 Android 上编译需要的配置文件,相当于 build.gradle,详细细节后面会讲到。

  • Application.mk:上代码

APP_PLATFORM := android-14 //指定 android 系统
APP_ABI := armeabi-v7a // 指定生成哪个架构的 so

更多详情

https://developer.android.com/ndk/guides/application_mk?hl=zh-cn

  • NDK :Android 平台上用来编译 C/C++库的工具

https://developer.android.com/ndk/downloads/

JNI 在 Android Studio 搭建

  • 创建一个子module
创建 java 层代码,新建一个HelloWorld 类准备和 c层对接,代码如下:
public class HelloWorld {

    static {
        try {
            System.loadLibrary("helloworld");
        } catch (Exception e) {
        }
    }
    private volatile static HelloWorld instance;
    private HelloWorld() {
    }
    public static HelloWorld getInstance() {
        if(instance == null) {
            synchronized (HelloWorld.class) {
                if(instance == null) {
                    instance = new HelloWorld();
                }
            }
        }
        return  instance;
    }


  public native String nativeGetString();

}

很明显上面类分成三部分:

  • 有 static 代码块,调用了System.loadLibrary("helloworld");这句代码代表着,使用这个类之前都会去加载libhelloworld.so 这个动态库,注意.so前面有lib。那这个动态库如何生成,后面讲。

  • 这个类是一个单例

  • 有一个 native 的方法  public native String nativeGetString();这个方法的实现在 c 层。所以接下来我们要构建 c 层的代码。

  • 接着在子module的目录下建立一个叫做 jni 的文件夹
例如:
  • 创建 c 代码,和配置文件
看下图的位置:
  • 生成一个 helloworld_android.c
代码如下:对接 java 层 ,下面的方法JNIEXPORT jstring JNICALL Java_com_tct_helloworld_nativeGetString(JNIEnv *env, jobject obj) 是 java 层 public native String nativeGetString(); 方法的 代码实现:
#获取当前目录的相对路径,也就是当前文件的父路径
LOCAL_PATH := $(call my-dir)
#清除当前的所有变量值
include $(CLEAR_VARS)

#本模块需要调用到的接口,也就是.h 文件
#LOCAL_C_INCLUDES := XXX

#本模块需要编译到的 c 文件
LOCAL_SRC_FILES := helloworld_android.c

#加入第三方库log库,NDK 自带的
LOCAL_LDLIBS := -llog

#生成库的名字。最终生成 libhelloworld
LOCAL_MODULE    := helloworld

#生成的是动态库.so
include $(BUILD_SHARED_LIBRARY)


#生成的是动态库.a
#include $(BUILD_STATIC_LIBRARY)
  • 生成并编写 Application.mk

APP_ABI := armeabi-v7a //生成 armeabi-v7a  的 so
APP_PLATFORM := android-21 //指定 tagerSDK
  • 接下来配置子 module 的 build.gradle 和 NDK
apply plugin: 'com.android.library'

android {
    compileSdkVersion 27

    externalNativeBuild.ndkBuild {
        path "src/main/jni/Android.mk" //指定c 层的 mk 文件的位置
    }
    defaultConfig {
        versionCode 1
        versionName "1.0"

        sourceSets {
            main {
                jni.srcDirs = [] //run 的时候不会重新编译 jni ,只有make 的时候生效
            }
        }
        ndk {
            abiFilters "armeabi-v7a"//让APK只包含指定的ABI
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}
  • 根目录下的 local.properties,配置自己的 NDK 路径

ndk.dir=G\:\\AndroidNDK\\android-ndk-r16b
  • 把项目跑起来
make 一下子 module,把项目编译一下。把.so 和 .aar 一次编译出来。
观察编译完毕的目录结构,aar是出来了,但是好像没有发现 so 的踪影。
解压 aar (aar 其实就是 zip 压缩,只是谷歌把它换了个后缀名)。
  • 最后写个MainActivity.java 调用一下接口
调用接口代码
 @OnClick(R.id.btnTestNDKCrash)
    void testNDKCrash(){
        String ret = HelloWorld.getInstance().nativeGetString();
        System.out.println("test "+ret);
    }

发现崩溃了,如何定位并且解决?先看log。

定位并解决问题

命令行:

G:\AndroidNDK\android-ndk-r16b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line 
-e D:\StadyProject\OpenCode\breakpad-for-android-master\sample\helloworld\build\intermediates\ndkBuild\debug\obj\local\armeabi-v7a\libhelloworld.so 00000fb9

打开你的 Terminal 把上面的命令输进去,就可以看到闪退的代码行了:

定位奔溃的代码行:

G:\AndroidNDK\android-ndk-r16b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line -e

目标文件 so 库的位置,so一个存在 aar ,一个存在 build 目录下面,位置比较深,但是都是固定目录,可以找到:

D:\StadyProject\OpenCode\breakpad-for-android-master\sample\helloworld\build\intermediates\ndkBuild\debug\obj\local\armeabi-v7a\libhelloworld.so

奔溃的 内存位置:

00000fb9

崩溃的代码行:

这句代码的意思是把 helloworld 字符串赋值到 foo 这个变量中去。但是少传了一个参数导致崩溃。 看下面两个函数的不同处

snprintf(foo, LEN, "%s""helloworld");//最多传入 foo 能承载的字符数,多了一个参数
sprintf(foo, "%s""helloworld");//无指定写入多少字符

那么改成以下代码,就可以了

#define LEN 64
snprintf(foo, LEN, "%s""helloworld");

再回顾一下 java层代码:

  @OnClick(R.id.btnTestNDKCrash)
    void testNDKCrash(){
        String ret = HelloWorld.getInstance().nativeGetString();
        System.out.println("test "+ret);
    }

跑起来logcat:

JNI 类型,方法对照表

  • 基本类型对照表

  • jni 层使用 java 类方法名称

JNI 场景实践

由于上面看了方法的对照表,下面讲解如何使用:

  • java 调用到 C 层
// JAVA 层方法
public native String nativeGetString(String tmp);
// 对应 JNI 层方法
JNIEXPORT jstring JNICALL
Java_com_tct_helloworld_HelloWorld_nativeGetString(JNIEnv *env, jobject obj,jstring jtmp) 
{
}

// JAVA 层方法
public native void nativeGetString(Model tmp);
// 对应 JNI 层方法
JNIEXPORT void JNICALL
Java_com_tct_helloworld_HelloWorld_nativeGetString(JNIEnv *env, jobject obj,jobject jmod) 
{
}

JNIEnv *env 是 JNI 中 java 线程的上下文,每一个线程都有一个 env。

jobject obj 代表的 java 的对象,从java 哪个对象调用下来的,就是哪对象。

  • C 层解析 java 类中的属性值,转成 C 层可使用的类型
//java 类
public class Model {

    public int code;
    public String name;

    public Model(int code, String name) {
        this.code = code;
        this.name = name;
    }
}

// JAVA 层方法
public native void nativeGetString(Model tmp);
// 对应 JNI 层方法
JNIEXPORT void JNICALL
Java_com_tct_helloworld_HelloWorld_nativeGetString(JNIEnv *env, jobject obj,jobject jmodel) 
{

   jclass jmodelClass = (*env)->GetObjectClass(env, jmodel);

    if (jmodelClass == 0) {
        return;
    }
    //获取变量 code 的值
    jfieldID fidCode = (*env)->GetFieldID(env, jmodelClass, "code""I");
    int code = (*env)->GetIntField(env, jmodel, fidCode);

   //获取变量 name 的值
    jfieldID fidName = (*env)->GetFieldID(env, jmodelClass, "name",
                                           "Ljava/lang/String;");
    jstring jname = (jstring)(*env)->GetObjectField(env, jmodel, fidName);
    char *name = (*env)->GetStringUTFChars(env, jname, 0);

    // ..

    //使用完毕,char * 需要回收
    (*env)->ReleaseStringUTFChars(env, jname, name);
    // 自己生成的 jclass 需要回收,以及其他的引用也是需要的,局部变量不能超512 个,特别是在 for 循环体内要及时回收
    (*env)->DeleteLocalRef(env, jmodelClass);
}
  • C 层返回 java 对象
//java 层方法
private volatile static HelloWorld instance;
private HelloWorld() {
}
public static HelloWorld getInstance() {
    if(instance == null) {
       synchronized (HelloWorld.class) {
         if(instance == null) {
                instance = new HelloWorld();
           }
        }
   }
    return  instance;
}
public native static HelloWorld nativeGetInstance();

//C层方法
JNIEXPORT jobject JNICALL Java_com_tct_helloworld_HelloWorld_nativeGetInstance
        (JNIEnv *env, jclass cls) 
{
    //找到class
    jclass cls1 = (*env)->FindClass(env, "com/tct/helloworld/HelloWorld");
    //找到构造函数的方法ID
    jmethodID cid = (*env)->GetMethodID(env, cls1, "<init>""()V");

    //生成一个对象返回
    jobject jInstance = (*env)->NewObject(env, cls1, cid);

    return jInstance;
}


// MainActivity.java 的调用方法
  @OnClick(R.id.btnTestNDKCrash)
    void testNDKCrash(){
        if(HelloWorld.getInstance() == HelloWorld.nativeGetInstance()) {
            System.out.println("HelloWorld instance true");
        } else {
            System.out.println("HelloWorld instance false");
        }

    }

得出 log:

I/System.out: HelloWorld instance false

原来不仅仅反射机制能破解单例, JNI 也是可以破解单例。

  • C 层返回 java 对象数组
//java 层代码
public native static HelloWorld[] nativeGetInstanceArray();
// c 层代码   
JNIEXPORT jobjectArray JNICALL Java_com_tct_helloworld_HelloWorld_nativeGetInstanceArray
        (JNIEnv *env, jclass cls) 
{
    jclass cls1 = (*env)->FindClass(env, "com/tct/helloworld/HelloWorld");
    jmethodID cid = (*env)->GetMethodID(env, cls1, "<init>""()V");
    jsize len = 10;
    jobjectArray mjobjectArray;
    //新建object数组
    mjobjectArray = (*env)->NewObjectArray(env, len, cls1, 0);
    for (int i = 0; i < len; ++i) {
        jobject jInstance = (*env)->NewObject(env, cls1, cid);
        (*env)->SetObjectArrayElement(env, mjobjectArray, i, jInstance);
        //回收,局部引用不能超过512个
        (*env)->DeleteLocalRef(env, jInstance);
    }

    (*env)->DeleteLocalRef(env, cls1);
    return mjobjectArray;
}

//MainActivity.java 调用
  @OnClick(R.id.btnTestNDKCrash)
    void testNDKCrash(){
        HelloWorld[] HelloWorlds = HelloWorld.getInstance().nativeGetInstanceArray();
        System.out.println("HelloWorld arrays length:"+HelloWorlds.length);
    }

log:

I/System.out: HelloWorld arrays length:10
  • C 层回调到 java 层
//java 层方法

public class TestBean {

    public int code;
    public String name;

    public TestBean(int code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public String toString() {
        return "TestBean{" +
                "code=" + code +
                ", name='" + name + '\'' +
                '}';
    }
}

 public interface HelloWorldListener {
        public void onLinstener(TestBean testBean);
}

public native  void nativeGetInstanceByThread(HelloWorldListener listener);




//c 层方法  



//jni 当前上下文,可用于当前 native 线程加入java 线程,用于回调,或者是获取 jvm 线程 上下文
JavaVM *g_VM;
//用来 findClass
jobject gClassLoader;
jmethodID gFindClassMethod;

//获取jvm 上下文
JNIEnv *getEnv() {
    JNIEnv *env;
    int status = (*g_VM)->GetEnv(g_VM, (void **) &env, JNI_VERSION_1_6);
    if (status < 0) {
        status = (*g_VM)->AttachCurrentThread(g_VM, &env, NULL);
        if (status < 0) {
            return NULL;
        }
    }
    return env;
}

/**
*  java 层调用 System.loadLibrary(); 的时候就会调用这个方法,此方法的目的是 找到classloader的对象,还有类加载的方法ID
*/

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
    g_VM = pjvm;  // cache the JavaVM pointer
    JNIEnv *env = getEnv();
    //replace with one of your classes in the line below
    jclass randomClass = (*env)->FindClass(env, "com/tct/helloworld/HelloWorld");
    jclass classClass = (*env)->GetObjectClass(env, randomClass);
    jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
    jclass getClassLoaderMethod = (*env)->GetMethodID(env, classClass, "getClassLoader",
                                                      "()Ljava/lang/ClassLoader;");
    gClassLoader = (*env)->NewGlobalRef(env, (*env)->CallObjectMethod(env, randomClass,
                                                                      getClassLoaderMethod));
    gFindClassMethod = (*env)->GetMethodID(env, classLoaderClass, "findClass",
                                           "(Ljava/lang/String;)Ljava/lang/Class;");

    return JNI_VERSION_1_6;
}

//调用ClassLoder 去找到对应的类,在linux 线程是独立于JVM ,所以一般的 findClass 是找不到jvm中的类。只能使用八大基本类型。
jclass GlobalFindClass(const char* name) {
    JNIEnv* env = getEnv();
    return (jclass)((*env)->CallObjectMethod(env,gClassLoader, gFindClassMethod, (*env)->NewStringUTF(env,name)));
}

void test_process(void *p) {
    jobject callBack = (jobject)p;
    JNIEnv *env;
    jboolean mNeedDetach;
    //获取当前native线程是否有没有被附加到jvm环境中
    int getEnvStat = (*g_VM)->GetEnv(g_VM, (void **) &env, JNI_VERSION_1_6);
    if (getEnvStat == JNI_EDETACHED) {
        //如果没有, 主动附加到jvm环境中,获取到env
        if ((*g_VM)->AttachCurrentThread(g_VM, &env, NULL) != 0) {
            return;
        }
        mNeedDetach = JNI_TRUE;
    }
    jclass cls = GlobalFindClass( "com/tct/helloworld/TestBean");
    if (cls == 0) {
        LOGI("native cls= %ld", cls);
        return;
    }
    jmethodID cid = (*env)->GetMethodID(env, cls, "<init>""(ILjava/lang/String;)V");
    jstring name = (*env)->NewStringUTF(env,"helloworld");
    jobject jInstance = (*env)->NewObject(env, cls, cid,(jint)1, name);


    //获取回调的类
    jclass  jcallBackClass = (*env)->GetObjectClass(env,callBack);

    //通过回调的类找到回调的方法
    jmethodID callbackid = (*getEnv())->GetMethodID(env, jcallBackClass, "onLinstener""(Lcom/tct/helloworld/TestBean;)V");
    if(callbackid ==0) {
        return;
    }

    //调用回调的方法
    (*env)->CallVoidMethod(env,callBack,callbackid,jInstance);

    (*env)->DeleteGlobalRef(env, callBack);
    (*env)->DeleteLocalRef(env, jcallBackClass);
    (*env)->DeleteLocalRef(env, jInstance);
    (*env)->DeleteLocalRef(env, cls);
    (*env)->DeleteLocalRef(env, name);

    //释放当前线程
    if (mNeedDetach) {
        (*g_VM)->DetachCurrentThread(g_VM);
    }

}

int start_test_thread(jobject listener) {
    pthread_t tid;
    if (0 != (pthread_create(&tid, NULL, test_process, listener))) {
        return -1;
    } else {
        pthread_detach(tid); //设置成 分离线程,线程跑完自己回收内存
    }
    return 0;
}

JNIEXPORT void JNICALL Java_com_tct_helloworld_HelloWorld_nativeGetInstanceByThread
        (JNIEnv *env, jobject obj,jobject jListener) 
{
// 这里的内存区域属于 native 栈中。跑完这个方法,局部变量都会被回收。所以需要使用 NewGlobalRef 对 jListener 生成一个全局引用(linux 堆中)
    jobject callback = (*env)->NewGlobalRef(env, jListener);
    //开启线程
    start_test_thread(callback);
}

JNI Java 和 C++ 无缝对接

以上实践都是 java 和 C 的对接。然而 java 是面向对象,C 是面向过程没有对象的概念。
举个场景例子:

如果 java 层需要发起A、B个线程 到C层去请求数据,并且需要各自提供 请求、取消的接口。
要实现多线程的取消接口,如果使用 C 封装 JNI,就需要提供链表(或者其他集合的数据结构)把每一个线程的 Tid(java),和请求绑定起来,取消的时候通过链表找到该线程的请求把柄,通过把柄取消。期间你会遇到链表插入删除,多线程锁,还得多个链表的全局引用。非常麻烦。

然而 java 就是为了避免这种麻烦,实现高效率编程。面向对象诞生了。

那么如何从 java -> C++->C 进行调用。上流程图,上代码:

java 层对接类 HelloWorld.java

public class HelloWorld {
    //加入一个变量 long 型保存 C++ 对象的地址
    public long mNativeContext = 0L;
    //类被创建,相对应的 JNI 也创建一个类
    public HelloWorld() {
        init();
    }
    public native void init();
    //..
}

JNI 层新建两个文件:HelloWorld.cpp、HelloWorld.h。

HelloWorld.cpp 代码

#include "HelloWorld.h"

extern "C" {

}

HelloWorld::HelloWorld() {
}

HelloWorld::~HelloWorld() {

}

 char * HelloWorld::getString() {
     return  "HelloWorld";

}

HelloWorld.h 代码

#ifndef HelloWorld_H
#define HelloWorld_H


class HelloWorld
{

public:
    HelloWorld();
    ~HelloWorld();
    char * getString();
};

#endif

JNI 层接口 helloworld_android.c 代码

//创建一个结构体存放对象地址
typedef struct {
    jfieldID context;
} fields_t;

static fields_t fields;

//  System.loadLibrary("helloworld");触发被调用的方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
    JNIEnv *env = getEnv();
    //获取 java 层 mNativeContext 变量的 ID ,并赋值到 fields.context 这个全局变量。
    fields.context = env->GetFieldID(randomClass, "mNativeContext""J");
    // ...
    return JNI_VERSION_1_6;
}

JNIEXPORT void  JNICALL Java_com_tct_helloworld_HelloWorld_init
        (JNIEnv *env, jobject obj) 
{
  //初始化,HelloWorld 指针对象,并且强转指针为 long 型,赋值到 对应的java 对象中 mNativeContext 的变量中去
    HelloWorld *mHelloWorld = new HelloWorld();
    env->SetLongField(obj, fields.context, (long)mHelloWorld);
}

最后验证一下:

//java 层代码
//MainActivity.java
System.out.println("test1" +new HelloWorld().nativeGetStringByObject());
System.out.println("test2" +new HelloWorld().nativeGetStringByObject());

//HelloWorld.java
public native String nativeGetStringByObject();

//C 层代码 
static HelloWorld *getObject(JNIEnv *env, jobject thiz) {
    // No lock is needed, since it is called internally by other methods that are protected
    HelloWorld *retriever = (HelloWorld *) env->GetLongField(thiz,fields.context);
    return retriever;
}

JNIEXPORT jstring  JNICALL Java_com_tct_helloworld_HelloWorld_nativeGetStringByObject(JNIEnv *env, jobject obj) {
  char * p = getObject(env,obj)->getString();
  return env->NewStringUTF(p);
}

log :

I/System.out: test1HelloWorld
I/System.out: test2HelloWorld

JNI 开源实战

对于 JNI 的一些的基本知识基本就讲完了。JNI 的用途为 java 开辟了另一扇大门,所有能在C 上面实现的。都能拿过来给Android平台上使用。

譬如以下一些 C库:

  • 音视频播放库

  • 高斯模糊库

  • openCV 人脸识别,车牌号码识别

  • 苹果的 AirPlay协议 蓝牙耳机

接下来实战一个 bilibili/ijkPlayer音视频解码库的开源代码。传送门地址:

https://www.jianshu.com/p/5aae140956ee

注: 感谢 

http://www.cnblogs.com/daniel-shen/archive/2006/10/16/530587.html

提供表格


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存