android-759b1c是什么,Android中C运用 讲解(一)

前言

我不会C++,没问题,跟凯哥一起学(七)通过前面几篇文章,我相信大家都对C++不再陌生,之前的一切不是为了让你精通C基础,而是让你对C有一个了解,有很多大佬都对我说语言就是API,几天就可以OK,主要看你怎么用。今天我们就来打开新的篇章,运用C遨游于安卓平台之中。

NDK 入门指南

Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具。 能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:

在平台之间移植其应用。

重复使用现有库,或者提供其自己的库供重复使用。

在某些情况下提高性能,特别是像游戏这种计算密集型应用。

Java:Android 构建过程从 Java 来源生成 .dex (Dalvik EXecutable) 文件,这些文件是 Android OS 在 Dalvik 虚拟机(“DVM”)中运行的文件。 即使您的应用根本未包含任何 Java 源代码,构建过程仍会生成原生组件在其中运行的 .dex 可执行文件。

开发 Java 组件时,使用 native 关键字指示以原生代码形式实现的方法。 例如,以下函数声明向编译器告知实现在原生库中:

public native int add(int x, int y)

原生共享库:NDK 从原生源代码构建这些库或 .so 文件。

注:如果两个库使用相同的签名实现各自的方法,就会发生关联错误。 在 C 语言中,“签名”只表示方法名称。在 C++ 中,“签名”不仅表示方法名称,还表示其参数名称和类型。

原生静态库:NDK 也可构建静态库或 .a 文件,您可以关联到其他库。

Java 原生接口 (JNI):JNI 是 Java 和 C++ 组件用以互相沟通的接口。 本指南假设您具备 JNI 知识;如需了解相关信息,请查阅 Java 原生接口规范。

应用二进制界面 (ABI):ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互。 NDK 根据这些定义构建 .so 文件。 不同的 ABI 对应不同的架构:NDK 包含对 ARMEABI(默认)、MIPS 和 x86 的 ABI 支持。 如需了解详细信息,请参阅 ABI 管理。

清单:如果您要编写没有 Java 组件的应用,必须在清单中声明 [NativeActivity](https://developer.android.com/reference/android/app/NativeActivity.html?hl=zh-cn) 类。原生 Activity 和应用在“使用 native_activity.h 接口”下提供了如何执行此操作的详细信息。

下面两个项目仅在使用 ndk-build 脚本构建时以及使用 ndk-gdb 脚本调试时才需要。

Android.mk:必须在 jni 文件夹内创建 Android.mk 配置文件。 ndk-build 脚本将查看此文件,其中定义了模块及其名称、要编译的源文件、版本标志以及要链接的库。

Application.mk:此文件枚举并描述您的应用需要的模块。 这些信息包括:

用于针对特定平台进行编译的 ABI。

工具链。

要包含的标准库(静态和动态 STLport 或默认系统)。

流程

为 Android 开发原生应用的一般流程如下:

设计应用,确定要在 Java 中实现的部分,以及要以原生代码形式实现的部分。

注:虽然可以完全避免 Java,但您可能发现,Android Java 框架对于包括控制显示和 UI 在内的任务很有用。

像创建任何其他 Android 项目一样创建一个 Android 应用项目。

如果要编写纯原生应用,请在 AndroidManifest.xml 中声明 [NativeActivity](https://developer.android.com/reference/android/app/NativeActivity.html?hl=zh-cn) 类。 如需了解详细信息,请参阅原生 Activity 和应用。

在“JNI”目录中创建一个描述原生库的 Android.mk 文件,包括名称、标志、链接库和要编译的源文件。

或者,也可以创建一个配置目标 ABI、 工具链、发行/调试模式和 STL 的 Application.mk 文件。对于其中任何您未指明的项目,将分别使用以下默认值:

ABI:armeabi

工具链:GCC 4.8

模式:发行

STL:系统

将原生来源置于项目的 jni 目录下。

使用 ndk-build 编译原生(.so、.a)库。

构建 Java 组件,生成可执行 .dex 文件。

将所有内容封装到一个 APK 文件中,包含 .so、.dex 以及应用运行所需的其他文件。

JNI官方使用指南

http://hukai.me/android-training-course-in-chinese/performance/perf-jni/index.html

简介:JNI全称Java Native Interface。它为托管代码(使用Java编程语言编写)与本地代码(使用C/C++编写)提供了一种交互方式。它是与厂商无关的(vendor-neutral),支持从动态共享库中加载代码,虽然这样会稍显麻烦,但有时这是相当有效的。

如果你对JNI还不是太熟悉,可以先通读Java Native Interface Specification这篇文章来对JNI如何工作以及哪些特性可用有个大致的印象。这种接口的一些方面不能立即一读就显而易见,所以你会发现接下来的几个章节很有用处。

JavaVM 及 JNIEnv

JNI定义了两种关键数据结构,“JavaVM”和“JNIEnv”。它们本质上都是指向函数表指针的指针(在C++版本中,它们被定义为类,该类包含一个指向函数表的指针,以及一系列可以通过这个函数表间接地访问对应的JNI函数的成员函数)。JavaVM提供“调用接口(invocation interface)”函数, 允许你创建和销毁一个JavaVM。理论上你可以在一个进程中拥有多个JavaVM对象,但安卓只允许一个。

JNIEnv提供了大部分JNI功能。你定义的所有本地函数都会接收JNIEnv作为第一个参数。

JNIEnv是用作线程局部存储。因此,你不能在线程间共享一个JNIEnv变量。如果在一段代码中没有其它办法获得它的JNIEnv,你可以共享JavaVM对象,使用GetEnv来取得该线程下的JNIEnv(如果该线程有一个JavaVM的话;见下面的AttachCurrentThread)。

JNIEnv和JavaVM的在C声明是不同于在C++的声明。头文件“jni.h”根据它是以C还是以C++模式包含来提供不同的类型定义(typedefs)。因此,不建议把JNIEnv参数放到可能被两种语言引入的头文件中(换一句话说:如果你的头文件需要#ifdef __cplusplus,你可能不得不在任何涉及到JNIEnv的内容处都要做些额外的工作)。

线程

所有的线程都是Linux线程,由内核统一调度。它们通常从托管代码中启动(使用Thread.start),但它们也能够在其他任何地方创建,然后连接(attach)到JavaVM。例如,一个用pthread_create启动的线程能够使用JNI AttachCurrentThread 或 AttachCurrentThreadAsDaemon函数连接到JavaVM。在一个线程成功连接(attach)之前,它没有JNIEnv,不能够调用JNI函数。

连接一个本地环境创建的线程会触发构造一个java.lang.Thread对象,然后其被添加到主线程群组(main ThreadGroup),以让调试器可以探测到。对一个已经连接的线程使用AttachCurrentThread不做任何操作(no-op)。

安卓不能中止正在执行本地代码的线程。如果正在进行垃圾回收,或者调试器已发出了中止请求,安卓会在下一次调用JNI函数的时候中止线程。

连接过的(attached)线程在它们退出之前必须通过JNI调用DetachCurrentThread。如果你觉得直接这样编写不太优雅,在安卓2.0(Eclair)及以上, 你可以使用pthread_key_create来定义一个析构函数,它将会在线程退出时被调用,你可以在那儿调用DetachCurrentThread (使用生成的key与pthread_setspecific将JNIEnv存储到线程局部空间内;这样JNIEnv能够作为参数传入到析构函数当中去)。

jclass, jmethodID, jfieldID

如果你想在本地代码中访问一个对象的字段(field),你可以像下面这样做:

对于类,使用FindClass获得类对象的引用

对于字段,使用GetFieldId获得字段ID

使用对应的方法(例如GetIntField)获取字段下面的值

类似地,要调用一个方法,你首先得获得一个类对象的引用,然后是方法ID(method ID)。这些ID通常是指向运行时内部数据结构。查找到它们需要些字符串比较,但一旦你实际去执行它们获得字段或者做方法调用是非常快的。

如果性能是你看重的,那么一旦查找出这些值之后在你的本地代码中缓存这些结果是非常有用的。因为每个进程当中的JavaVM是存在限制的,存储这些数据到本地静态数据结构中是非常合理的。

类引用(class reference),字段ID(field ID)以及方法ID(method ID)在类被卸载前都是有效的。如果与一个类加载器(ClassLoader)相关的所有类都能够被垃圾回收,但是这种情况在安卓上是罕见甚至不可能出现,只有这时类才被卸载。注意虽然jclass是一个类引用,但是必须要调用NewGlobalRef保护起来(见下个章节)。

当一个类被加载时如果你想缓存些ID,而后当这个类被卸载后再次载入时能够自动地更新这些缓存ID,正确做法是在对应的类中添加一段像下面的代码来初始化这些ID:

/*

* 我们在一个类初始化时调用本地方法来缓存一些字段的偏移信息

* 这个本地方法查找并缓存你感兴趣的class/field/method ID

* 失败时抛出异常

*/

private static native void nativeInit();

static {

nativeInit();

}

在你的C/C++代码中创建一个nativeClassInit方法以完成ID查找的工作。当这个类被初始化时这段代码将会执行一次。当这个类被卸载后而后再次载入时,这段代码将会再次执行。

局部和全局引用

每个传入本地方法的参数,以及大部分JNI函数返回的每个对象都是“局部引用”。这意味着它只在当前线程的当前方法执行期间有效。即使这个对象本身在本地方法返回之后仍然存在,这个引用也是无效的。

这同样适用于所有jobject的子类,包括jclass,jstring,以及jarray(当JNI扩展检查是打开的时候,运行时会警告你对大部分对象引用的误用)。

如果你想持有一个引用更长的时间,你就必须使用一个全局(“global”)引用了。NewGlobalRef函数以一个局部引用作为参数并且返回一个全局引用。全局引用能够保证在你调用DeleteGlobalRef前都是有效的。

这种模式通常被用在缓存一个从FindClass返回的jclass对象的时候,例如:

jclass localClass = env->FindClass("MyClass");

jclass globalClass = reinterpret_cast(env->NewGlobalRef(localClass));

所有的JNI方法都接收局部引用和全局引用作为参数。相同对象的引用却可能具有不同的值。例如,用相同对象连续地调用NewGlobalRef得到返回值可能是不同的。为了检查两个引用是否指向的是同一个对象,你必须使用IsSameObject函数。绝不要在本地代码中用==符号来比较两个引用。

UTF-8、UTF-16 字符串

Java编程语言使用UTF-16格式。为了便利,JNI也提供了支持变形UTF-8(Modified UTF-8)的方法。这种变形编码对于C代码是非常有用的,因为它将\u0000编码成0xc0 0x80,而不是0x00。最惬意的事情是你能在具有C风格的以\0结束的字符串上计数,同时兼容标准的libc字符串函数。不好的一面是你不能传入随意的UTF-8数据到JNI函数而还指望它正常工作。

如果可能的话,直接操作UTF-16字符串通常更快些。安卓当前在调用GetStringChars时不需要拷贝,而GetStringUTFChars需要一次分配并且转换为UTF-8格式。注意UTF-16字符串不是以零终止字符串,\u0000是被允许的,所以你需要像对jchar指针一样地处理字符串的长度。

不要忘记Release你Get的字符串。这些字符串函数返回jchar或者jbyte,都是指向基本数据类型的C格式的指针而不是局部引用。它们在Release调用之前都保证有效,这意味着当本地方法返回时它们并不主动释放。

传入NewStringUTF函数的数据必须是变形UTF-8格式。一种常见的错误情况是,从文件或者网络流中读取出的字符数据,没有过滤直接使用NewStringUTF处理。除非你确定数据是7位的ASCII格式,否则你需要剔除超出7位ASCII编码范围(high-ASCII)的字符或者将它们转换为对应的变形UTF-8格式。如果你没那样做,UTF-16的转换结果可能不会是你想要的结果。JNI扩展检查将会扫描字符串,然后警告你那些无效的数据,但是它们将不会发现所有潜在的风险。

本地库

你可以使用标准的System.loadLibrary方法来从共享库中加载本地代码。在你的本地代码中较好的做法是:

在一个静态类初始化时调用System.loadLibrary(见之前的一个例子中,当中就使用了nativeClassInit)。参数是“未加修饰(undecorated)”的库名称,因此要加载“libfubar.so”,你需要传入“fubar”。

提供一个本地函数:jint JNI_OnLoad(JavaVM vm, void reserved)

在JNI_OnLoad中,注册所有你的本地方法。你应该声明方法为“静态的(static)”因此名称不会占据设备上符号表的空间。

JNI_OnLoad函数在C++中的写法如下:

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

JNIEnv* env;

if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {

return -1;

}

// 使用env->FindClass得到jclass

// 使用env->RegisterNatives注册本地方法

return JNI_VERSION_1_6;

}

你也可以使用共享库的全路径来调用System.load。对于Android app,你也许会发现从context对象中得到应用私有数据存储的全路径是非常有用的。

上面是推荐的方式,但不是仅有的实现方式。显式注册不是必须的,提供一个JNI_OnLoad函数也不是必须的。你可以使用基于特殊命名的“发现(discovery)”模式来注册本地方法(更多细节见:JNI spec),虽然这并不可取。因为如果一个方法的签名错误,在这个方法实际第一次被调用之前你是不会知道的。

关于JNI_OnLoad另一点注意的是:任何你在JNI_OnLoad中对FindClass的调用都发生在用作加载共享库的类加载器的上下文(context)中。一般FindClass使用与“调用栈”顶部方法相关的加载器,如果当中没有加载器(因为线程刚刚连接)则使用“系统(system)”类加载器。这就使得JNI_OnLoad成为一个查寻及缓存类引用很便利的地方。

64位机问题

Android当前设计为运行在32位的平台上。理论上它也能够构建为64位的系统,但那不是现在的目标。当与本地代码交互时,在大多数情况下这不是你需要担心的,但是如果你打算存储指针变量到对象的整型字段(integer field)这样的本地结构中,这就变得非常重要了。为了支持使用64位指针的架构,你需要使用long类型而不是int类型的字段来存储你的本地指针。

不支持的特性/向后兼容性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值