首页 文章 精选 留言 我的

精选列表

搜索[网站开发],共10000篇文章
优秀的个人博客,低调大师

Android官方开发文档Training系列课程中文版:Android的JNI相关

原文地址:http://android.xsoftlab.net/training/articles/perf-jni.html JNI的全称为Java Native Interface,中文意思是Java本地接口。它定义了Java代码与C/C++代码之间的交互方式。它是两者的桥梁,支持从动态共享库中加载代码。虽然有些复杂,但是它的执行效率还是蛮高的。 如果你对JNI还不太熟悉,那么可以通过Java Native Interface Specification来了解一下JNI的大致工作流程以及JNI的特性。 JavaVM与JNIEnv JNI定义了两个关键的数据结构:”JavaVM”与”JNIEnv”。这两个函数本质上都为指向函数指针的指针表。JavaVM提供了”接口调用”功能,该功能允许创建、销毁JavaVM。理论上每个进程可以拥有多个虚拟机,但是在Android中只允许出现一个。 JNIEnv提供了大部分的JNI功能。任何本地方法都以JNIEnv为第一回调参数。 JNIEnv用于线程局部存储。正出于这个原因,所以不能在线程间共享JNIEnv。如果不能够通过其它方式获取其对应的JNIEnv对象,那么应该先共享JavaVM,然后通过GetEnv函数获取该线程对应的JNIEnv(假设该线程拥有一个JNIEnv,具体请往下看)。 C与C++对JNIEnv和JavaVM的声明方式并不相同。头文件”jni.h”针对C或者C++提供了不同的类型定义。正因为这个原因,在头文件中包含JNIEnv参数并不是个明智的主意。 线程 Android中所有的线程都是Linux线程,都由内核执行。通常由受控代码启动(比如Thread.start),但是也可以由别的地方创建,然后再附加到JavaVM上启动。举个例子,线程可以由pthread_create函数创建,然后通过AttachCurrentThread或AttachCurrentThreadAsDaemon将其附加到JavaVM上执行。 Android并不会挂起正在执行本地代码的线程。如果垃圾收集正在进行,或者调试器发起了挂起请求,那么Android会在下次JNI调用时暂停线程。 通过JNI所附加的线程在退出前必须调用DetachCurrentThread函数。 jclass, jmethodID, 及jfieldID 如果需要在本地代码中访问对象的属性,那么需要执行以下操作: 通过FindClass获取类对象的引用 通过GetFieldID获得属性的ID 通过对应的方法获取对象的内容,比如GetIntField 相应的,如果要调用一个方法,首先获取类对象的引用,其次获取该方法的ID。ID通常只是指向了一个内部的运行时数据结构。查找这些方法通常需要进行若干次字符串比对,但是一旦找到,那么后期的获取属性或者方法调用都会非常的迅速。 如果性能对你很重要,那么在找到这些属性或者方法之后,应该将其缓存起来。因为Android中只允许每个进程有一个JavaVM的存在,所以将这些数据缓存在一个静态本地结构中是合理的。 类的引用、属性的ID、方法的ID在这个类被卸载之前都可以保证它们有效。一个类只有在这种情况下才会被卸载:该类所关联的ClassLoader也能被回收。虽然这几率很低,但是在Android中不是没有可能的。 如果想在类加载的时候将这些ID缓存下来,并在类被卸载之后再重新加载时还能重新缓存,最正确的方法是添加这样一段代码: /* * We use a class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private static native void nativeInit(); static { nativeInit(); } 在C/C++代码中创建一个名为nativeClassInit的方法,用于ID的查找与缓存。该方法会在类初始化的时候执行一次。就算是类被卸载后又重新加载,那么这个方法还是会被执行一次。 局部引用,全局引用 每个被回调到本地方法的参数,以及几乎所有的通过JNI方法返回的对象都是局部变量。这意味着当前线程中该方法内的所有局部变量都是合法的。在本地方法返回之后,虽然对象仍然存活,但是引用却是无效的。 这适用于jobject所有的子类:jclass, jstring, 以及jarray。 获取非局部变量的唯一方式就是通过NewGlobalRef及NewWeakGlobalRef函数获得。 如果需要长时间持有一段引用,那么必须使用全局引用。NewGlobalRef函数会将一个局部引用转换为一个全局引用。在调用DeleteGlobalRef方法之前,该全局引用一直有效。 这种模式通常用于缓存一个由FindClass返回的一个jclass对象: jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass)); 所有的JNI方法都可以以这两种引用为参数。不过引用相同的值可能有不同的结果。举个例子,以同一个引用为参数连续调用两次NewGlobalRef可能会得到不同的值。如果要查看两个引用是否指向了同一个对象,必须使用IsSameObject函数。绝不要在本地代码中使用”==”比较两个引用。 绝不要认为在本地代码中的对象引用是个常量或者是唯一的。一个32位的值所代表的对象的方法调用可能与下次调用就有所不同,这可能是因为两个不同的对象拥有相同的32位值。不要将jobject的值当做键使用。 程序员经常被要求不要过度的申请局部变量。这意味着如果你创建了大量的局部变量,那么应当通过DeleteLocalRef函数手动的释放它们,而不是让JNI为你做这些事情。 要注意jfieldIDs、jmethodID并不是对象引用,所以不能够将它们传给NewGlobalRef函数使用。GetStringUTFChars函数与GetByteArrayElements函数所返回的原始数据指针也同样不是对象。 一个不寻常的情况需要单独说明一下:如果通过AttachCurrentThread函数attach到了一个本地线程上,那么在该线程被detache之前,代码中所有的局部变量都不会被自动释放。任何创建的局部变量都需要手动删除。 UTF-8与UTF-16字符串 Java语言使用的是UTF-16字符串。为了方便起见,JNI所提供的方法工作在Modified UTF-8字符串下。修正后的编码对于C语言代码很有用,因为它将\u0000编码为了0xc0 0x80。 不要忘记释放你所获得的字符串。字符串函数会返回jchar* 或 jbyte*,它们是指向原始数据的指针,而不是本地引用。它们在被释放之前一直有效,这意味着在本地方法返回后,它们并没有被释放。 传给NewStringUTF函数的数据必须是Modified UTF-8格式。一个常见的错误就是从文件流或者网络流中读取字符串数据,然后没有过滤就直接交给了NewStringUTF函数进行处理。除非你知道这些数据是7位的ASCII,否则你需要剔除高位的ASCII字符串或者将它们转换为正确的Modified UTF-8格式。如果你不这么做,那么转换的结果可能不是你想看到的。额外的JNI检查会扫描字符串并会警告你这是无效的数据,但是它们不会捕获任何事情。 原始数组 JNI提供了用于访问对象数组的功能。然而,同一时间只能对一个元素进行访问,可以直接对数组今夕读写操作,就好像直接在C中声明的一样。 为了使JNI接口尽可能的高效,也不受虚拟机实现的限制,调用GetArrayElements的相关函数可以返回一个指向实际值的指针,或者可以申请一些内存以完成复制。无论哪种方法,所返回的指针都可以保证是有效的,直到相应的释放方法被触发。必须释放你所取得的每个数组。如果Get方法调取失败,也需要保证不要去释放一个空的指针对象。 你可以通过isCopy参数来检测一个数组是否是由指针所拷贝过来的,这一点很有用。 Release方法需要一个mode参数,这个参数有三种值。运行时执行的操作取决于它返回指向实际数据的指针或者指针的副本: 0 实际指针:非final修饰的数组对象 指针副本:拷贝后的数组数据,拷贝的缓冲区会被释放 JNI_COMMIT 实际指针:不做任何事情 指针副本:拷贝后的数组数据,拷贝的缓冲区不会被释放 JNI_ABORT 实际指针:非final修饰的数组对象。早些写入不会被中止。 指针副本:所拷贝的缓冲区被释放;缓冲区内的任何变更都会丢失。 检查isCopy标志的其中一个原因是需要知道在对数组作出变更之后是否需要调用JNI_COMMIT的相关释放方法,如果要更改一个正在作出变更以及读取数组内容的操作,那么可以根据该标志跳过这次操作。另一个可能的原因就是用于有效的处理JNI_ABORT。举个例子,你可能想要得到一个数组,然后对其修改之后将其传给一个函数。如果你知道JNI会为你做一个副本的话,那么就不需要创建另外的可编辑副本了。如果JNI传回的是原始数据,那么你自己需要创建一个副本。 一个常见的错误就是如果*isCopy是false,那么可以不调用相关释放方法。但是事实并非如此,如果没有申请拷贝缓冲区,那么原始数据内存必定会被一直占用,也不会被垃圾收集器回收。 还要注意的是,JNI_COMMIT并不会释放数组,你需要在另外的标志执行后再执行一次释放。 方法调用 JNI在方法使用上有两种方式,一种如下所示: jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); } 上面这段代码首先得到了一个数组,然后拷贝出len个字节的元素,最后将这个数组释放。根据实现的不同,Get调用会返回原始数据或者数据副本。在这个案例中,JNI_ABORT可以确保不出现第三个副本。 另一种实现则要更简单一些: env->GetByteArrayRegion(array, 0, len, buffer); 对于此有若干建议: - 减少JNI调用可以节省开销。 - 不要原始数据或者额外的数据拷贝。 - 降低程序员出错的风险–他们会在某些操作失败后忘记调用相关的释放方法。 类似的,你可以使用SetArrayRegion函数将数据拷贝到一个数组中,GetStringRegion函数或GetStringUTFRegion可以从String拷贝任意长度的字符。 异常 当异常出现时,请不要继续向下执行。代码应当注意到这些异常并返回,或者处理这些异常。 当异常发生时,只有以下JNI方法允许调用: DeleteGlobalRef DeleteGlobalRef DeleteLocalRef DeleteWeakGlobalRef ExceptionCheck ExceptionClear ExceptionDescribe ExceptionOccurred MonitorExit PopLocalFrame PushLocalFrame ReleaseArrayElements ReleasePrimitiveArrayCritical ReleaseStringChars ReleaseStringCritical ReleaseStringUTFChars 很多JNI函数都会抛出异常,不过只提供了一种很简单的检查方法。比如,如果NewString函数返回了一个非空的值,那么就不需要检查异常。然而,如果你调用一个方法,比如CallObjectMethod,那么就需要每次都检查一下异常,因为如果异常被抛出后,返回值是无效的。 主要注意的是,由中断所抛出的异常不会释放本地栈帧,Android目前也不支持C++异常。JNI的Throw与ThrowNew结构也只是在当前的线程设置了一个异常指针。当异常发生时也只是返回到代码调用处,异常也不会被正确的注意与处理。 本地代码可以通过ExceptionCheck函数或ExceptionOccurred函数捕获异常,并可以通过ExceptionClear函数清理这些异常。通常情况下,不处理这些异常会导致一些问题的出现。 JNI中并没有与Throwable相对应的映射函数,所以,如果你想获得异常字符串,那么就需要先找到Throwable类,然后查找相关的getMessage “()Ljava/lang/String;”方法ID,然后调用这些方法,如果返回的值是非空的话,再调用GetStringUTFChars函数来获得你想得到的异常字符串,最后将这些异常打印出来。 本地库 你可以通过标准的System.loadLibrary函数加载共享库中的本地代码。推荐获取本地代码的方法有: System.loadLibrary(),该方法唯一的参数是一个简要的库名,所以如果要加载”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<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } // Get jclass with env->FindClass. // Register methods with env->RegisterNatives. return JNI_VERSION_1_6; } 你也可以通过System.load函数外加库的全限定名来加载本地库。 使用JNI_OnLoad另一个需要注意的是:任何FindClass调用都会发生在类加载器的上下文环境中,该类加载器用于加载共享库。通常情况下,FindClass所用到的加载器位于解释栈的顶端,如果还没有加载器,那么它会使用系统的加载器。 64位的注意事项 Android目前运行于32位的平台上。虽然理论上可以为64位的平台构建系统,但是目前它不是主要的目标。大多数情况下,这不是你需要担心的事情,但是如果要将指针存储于本地结构中的一个对象的Int属性上,那么这就很值得关注了。为了支持64位指针结构,你需要将本地指针存储于一个Long属性中。 不支持特性与向后兼容 支持所有的JNI1.6特性,以及以下异常: - DefineClass 还没有实现。Android并没有使用Java的字节码以及类文件,所以传入二进制的类数据是不会被执行的。 如果需要兼容Android老的版本,那么应该检查以下部分: 动态查询本地函数 在Android 2.0之前,字符’$’在查找方法时不会被正确的转换为”_00024”。所以使用有关方法需要明确注册或者将内部类方法移出。 分离线程 在Android 2.0之前,无法使用pthread_key_create析构函数来避免”在退出之前必须分离线程”这项检查。 弱的全局引用 在Android 2.2之前,弱的全局引用还没有实现。之前的版本会拒绝使用它们。你可以使用Android平台版本来检测是否支持。 在Android 4.0之前,弱的全局引用只能被传入NewLocalRef, NewGlobalRef, 以及 DeleteWeakGlobalRef这几个函数。 从Android 4.0开始,弱的全局引用可以像其它JNI引用一样使用。 本地引用 在Android 4.0之前,本地引用实际上就是指针。在Android 4.0之后添加了必要的中间角色,以便更好的支持垃圾回收器的工作,不过这意味着有很多JNI的bug在老版本上无法察觉。查看JNI Local Reference Changes in ICS获取更多信息。 通过GetObjectRefType检查引用类型 在Android 4.0之前,由于直接指针的使用,无法正确的实现GetObjectRefType。我们通过弱的全局表、参数、本地表以及全局表进行查找。首先它会找到你的直接指针,并返回它所检查的引用类型。这意味着,如果你在全局的jclass上作用GetObjectRefType,而这个jclass以一个隐性参数传给了一个静态本地方法,那么你将会获得JNILocalRefType而不是JNIGlobalRefType。

优秀的个人博客,低调大师

Ubuntu16.04安装Docker1.12+开发实例+hello world+web应用容器

本次主要是详细记录Docker1.12在Ubuntu16.04上的安装过程,创建Docker组(避免每次敲命令都需要sudo),Docker常用的基本命令的总结,在容器中运行Hello world,以及创建一个基于Python Flask的web应用容器的全过程。 1.Docker1.12在Ubuntu16.04上安装 1.1.先决条件1,添加Docker源 wxl@wxl-pc:~$ sudo apt-get update 增加CA证书 wxl@wxl-pc:~$ sudo apt-get install apt-transport-https ca-certificates 添加GPG Key(一种加密手段) wxl@wxl-pc:~$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D 创建docker.list文件 wxl@wxl-pc:~$ sudo vim /etc/apt/sources.list.d/docker.list #添加Ubuntu16.04LST的入口 deb https://apt.dockerproject.org/repo ubuntu-xenial main 再次更新源 wxl@wxl-pc:~$ sudo apt-get update 以防万一,清除过时的源 wxl@wxl-pc:~$ sudo apt-get purge lxc-docker 验证下APT是从正确的库源下载应用的 wxl@wxl-pc:~$ apt-cache policy docker-engine 至此,可见已经配置好了Docker的源 1.2.先决条件2,安装aufs驱动linux-image-extra For Ubuntu Trusty, Wily, and Xenial, it’s recommended to install the linux-image-extra kernel package. The linux-image-extra package allows you use the aufs storage driver可以实现容器间可执行文件和运行库的共享。 更新源,会发现Hit:9 https://apt.dockerproject.org/repo ubuntu-xenial InRelease,也说明Docker在第一步1设置成功。 wxl@wxl-pc:~$ sudo apt-get update 安装 linux-image-extra wxl@wxl-pc:~$ sudo apt-get install linux-image-extra-$(uname -r) 1.3.安装Docker(如果先决条件1,2步正确完成了) 更新源 wxl@wxl-pc:~$ sudo apt-get update 通过apt命令在线安装docker wxl@wxl-pc:~$ sudo apt-get install docker-engine 开启docker的守护进程(Docker服务开启) wxl@wxl-pc:~$ sudo service docker start 国际惯例,用一个Hello world的来测试安装成功 wxl@wxl-pc:~$ sudo docker run hello-world 本地本来没有Hello World镜像,通过Docker源获取到,并成功现实Hello world。 查看正在运行的容器 sudo docker ps -ls 1.4.创建Docker用户组,避免使用sudo 如第一步最后“查看正在运行的容器”如果没有sudo,不以root身份权限运行查看容器命令则会报错Cannot connect to the Docker daemon. Is the docker daemon running on this host?如图 原因: The docker daemon binds to a Unix socket instead of a TCP port. By default that Unix socket is owned by the user root and other users can access it with sudo. For this reason, docker daemon always runs as the root user. To avoid having to use sudo when you use the docker command, create a Unix group called docker and add users to it. When the docker daemon starts, it makes the ownership of the Unix socket read/writable by the docker group. 创建用户组docker,可以避免使用sudo 将docker和wxl(王小雷用户名,在创建主机时默认用户名称是ubuntu)添加到一个组内 #默认是ubuntu用户 #wxl@wxl-pc:~$ sudo usermod -aG docker ubuntu # 将wxl的用户添加到docker用户组中,如果多个用户需要用空格隔开 如 wxl wxl1 wxl2用户 wxl@wxl-pc:~$ sudo usermod -aG docker wxl 注意需要重新启动计算机或者注销用户再登入,才能生效。这样就不需要使用sudo命令了。 那么,如何将wxl从docker用户组移除? sudo gpasswd -d wxl docker 如何删除刚才创建的docker用户组? sudo groupdel docker 如何创建和删除新用户,如用户newuser sudo adduser newuser sudo userdel newuser 1.5.如何更新Docker wxl@wxl-pc:~$ sudo apt-get upgrade docker-engine 1.6.如何卸载Docker wxl@wxl-pc:~$ sudo apt-get purge docker-engine 2.运行一个web应用–Python Flask 2.1.docker简单命令汇总如下: docker run ubuntu /bin/echo “hello world” -运行ubuntu镜像并且在命令窗口输出”hello world” docker run -t -i ubuntu /bin/bash -进入ubuntu这个镜像的bash命令窗口,可以操作本镜像ubuntu的命令如ls docker ps - 列出当前运行的容器 docker logs - 展示容器的标准的输出(比如hello world) docker stop - 停止正在运行的容器 docker version -可以查看守护的进程,docker版本以及go版本(docker本身是用go语言写的) 总结,可以看出docker的命令一般为 [sudo] docker [subcommand] [flags] [arguments] 如docker run -i -t ubuntu /bin/bash 2.2.开始运行Python Flask 运行Python Flask应用(这个过程可能很慢,根据网速而定,因为如果本地没有镜像training/webapp:latest会自动线上获取) 完成 查看运行中打容器通过 docker ps -l 注意:查看你打端口号,可能和我打不一样 我的是(把Terminal最大化容易识别) 指定端口号,通过Docker -p,如将32769更改为5000 浏览器访问 http://localhost:80 或者http://localhost/ 根据CONTAINER ID 或者 NAMES 来使用log和top命令,如我执行时产生的CONTAINER ID是83442361e61b,而NAMES是reverent_saha # 按Ctrl+c结束 查看log wxl@wxl-pc:~$ docker logs -f reverent_saha wxl@wxl-pc:~$ docker top reverent_saha #返回JSON文档查看配置和状态信息 wxl@wxl-pc:~$ docker inspect reverent_saha #通过特定JSON文档的元素查看特定的配置和状态信息,如IP wxl@wxl-pc:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' reverent_saha 开启/删除/当前的web应用容器 #关闭reverent_saha名称为的web应用容器 wxl@wxl-pc:~$ docker start reverent_saha #删除reverent_saha名称为的web应用容器(注意,容器必须是stop状态) wxl@wxl-pc:~$ docker rm reverent_saha 关闭web应用容器,通过docker ps -l 查看容器开启状态 wxl@wxl-pc:~$ docker stop reverent_saha #开启reverent_saha名称为的web应用容器 wxl@wxl-pc:~$ docker ps -l 此时,在打开 http://localhost/ 已经无法链接,因为停止来python flask的web应用。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册