LZ-Says:半夜睡觉滚了地上了,无奈之下醒来了,想想最近几天因为一个括号导致JNI迟迟不能开展,心里面无奈又崩溃,索性直接起来整完得了~
前言
当前毕业的时候,感觉自己掌握了全世界,随着参加工作的时间一天天的增长,突然觉得丫的,啥都不会啊,要学的东西还是太多太多了。不过近来被飞大姐洗脑成功,<font color=#FF0000>万事不过几行代码而已,干它~
So 今天为大家带来简单的jni配置,使用,以及运行我们的第一个简单小demo`
Hi Jni
总是再说jni,jni,那么jni到底是什么东西,我们一起来看看:
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。
JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
下面为大家附上官方Android平台架构图:
<center>可以看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的Libraries都是C/C++编写的,所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现。
Jni使用场景
当你开始着手准备一个使用JNI的项目时,请确认是否还有替代方案。应用程序使用JNI会带来一些副作用。下面给出几个方案,可以避免使用JNI的时候,达到与本地代码进行交互的效果:
1、JAVA程序和本地程序使用TCP/IP或者IPC进行交互。
2、当用JAVA程序连接本地数据库时,使用JDBC提供的API。
3、JAVA程序可以使用分布式对象技术,如JAVA IDL API。
这些方案的共同点是,JAVA和C处于不同的线程,或者不同的机器上。这样,当本地程序崩溃时,不会影响到JAVA程序。
下面这些场合中,同一进程内JNI的使用无法避免:
1、程序当中用到了JAVA API不提供的特殊系统环境才会有的特征。而跨进程操作又不现实。
2、你可能想访问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存,数据传递方面。
3、JAVA程序当中的一部分代码对效率要求非常高,如算法计算,图形渲染等。
<font color=#FF0000>总之,只有当你必须在同一进程中调用本地代码时,再使用JNI。
Jni缺陷
一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:
1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVA和C之间的耦合性
Jni作用
JNI可以这样与本地程序进行交互:
1、你可以使用JNI来实现“本地方法”(native methods),并在JAVA程序中调用它们。
2、JNI支持一个“调用接口”(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用“调用接口”执行JAVA语言编写的软件模块。
简单了解以上内容后,我们开启正题,在Android开发中,我们该怎么使用jni,或者说是在Android Studio中,我们该怎么使用jni呢?表急,往下瞅瞅~
话说,我们开发android应用程序基础不就是下载官方相关的SDK,ADT啥啥啥的,同理jni也一样。
现在为大家简单介绍NDK~如下。
Hi NDK
Android NDK 是一套允许使用原生代码语言(例如 C 和 C++)实现部分应用的工具集。在开发某些类型应用时,这有助于重复使用以这些语言编写的代码库。
同理,官方也为我们提供了一个小例子,简单走马观花看一下:
public class MyActivity extends Activity {
/**
* 使用 C/C++ 语言实现的原生方法
*/
public native void computeFoo();
}
native方法,当年看到这个东西感觉好高大上,没想到而今我也能玩玩,哈哈,GGG~
官方对于NDK是这样说的:
NDK 不适用于大多数初学的 Android 编程者,对许多类型的 Android 应用没什么价值。 因为它不可避免地会增加开发过程的复杂性,所以通常不值得使用。 但如果您需要执行以下操作,它可能很有用:
从设备获取卓越性能以用于计算密集型应用,例如游戏或物理模拟;
重复使用您自己或其他开发者的 C 或 C++ 库。
丫的,爷儿们好奇瞅瞅不行啊?
行~!
哈哈~
简单有个印象后,我们开始配置相关内容,为什么要说这个配置呢,主要有以下几个原因:
虽说配置是傻瓜式无脑操作,但是对于LZ小白这样的人来说,依然觉得是一件很高大上的事儿,何况,丫的,连配置都不会,还开发个卵子?
凡事儿亲历亲为,不经历,怎能有成长?
配置之前,我们还需要了解我们需要配置or下载哪儿些工具,以及这些东西都是干嘛的,不然稀里糊涂的,糟心。
同理,我们也需要去简单了解下使用NDK好处:
1、代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大;
2、可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的;
3、提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率;
4、便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。
NDK以及所需构建工具简介
NDK:这个还需要再说嘛?
CMake:外部构建工具;
CMake是一个开源的跨平台系列工具,旨在构建,测试和打包软件。
CMake用于使用简单的平台和编译器独立的配置文件来控制软件编译过程,并生成可以在选择的编译环境中使用的本地makefile和工作空间。
CMack工具套件由Kitware创建,以响应对开源项目(如ITK和VTK)的强大的跨平台构建环境的需求。
官方地址:https://cmake.org/ 有兴趣可以简单了解下~
- LLDB:Android Studio 上面调试本地代码的工具
LLDB是下一代高性能调试器。它被构建为一组可重用的组件,可以高度利用较大的LLVM项目中的现有库,例如Clang表达式解析器和LLVM反汇编程序。
在Mac OS X中,LLDB是Xcode中的默认调试器,支持在桌面和iOS设备和模拟器上调试C,Objective-C和C ++。
LLDB项目中的所有代码都可以使用标准的 LLVM许可证(一种开放源代码“BSD风格”)许可证。
LLDB目前将调试信息转换成clang类型,以便它可以利用clang编译器基础架构。这允许LLDB在表达式中支持最新的C,C++,Objective C和Objective C ++语言特性和运行时间,而无需重新实现任何此功能。在编写表达式的函数,拆卸指令和提取指令详细信息等时,还可以利用编译器来处理所有ABI细节。
主要优点包括:
- C,C ++,Objective C的最新语言支持 ;
- 可以声明局部变量和类型的多行表达式;
- 支持时使用JIT表达式;
- 当JIT不能使用时,评估表达式中间表示(IR)
以上简单了解下就好了,至于为啥要这么搞,就是为了方便以后有需要直接翻出来看看~
NDK配置(包含构建工具,调试工具)
<font color=#FF0000>下载安装NDK,有俩种方式,其实都一样,只是一个需要手动下载,解压,配置目录,一个Android Studio自动完成以上操作。
配置NDK俩种方式
手动下载NDK
NDK下载地址如下:
https://developer.android.google.cn/ndk/downloads/index.html
大家可自行选择相应版本进行下载。
下载完成后,解压本地目录,在Android Studio中配置路径即可,如下图所示:
<center>强大的Android Studio走起~
1.点击Project Structure,选择 Download Android NDK;
<center>2.耐心等待吧,LZ下载比较快,解压比较慢~
3.下载解压完成,自动录入地址,省事儿哈~
<center>到此,关于NDK下载安装俩种方式图解完毕~
我们看一下ndk目录各个作用,简单了解下。
- docs: 帮助文档
build/tools:linux的批处理文件
platforms:编译c代码需要使用的头文件和类库
prebuilt:预编译使用的二进制可执行文件
sample:jni的使用例子
source:ndk的源码
toolchains:工具链
ndk-build.cmd:编译打包c代码的一个指令,需要配置系统环境变量
配置构建工具以及调试工具
1.如下图所示,点击SDK Manager,选择下载安装CMake以及LLDB;
<center>简单看一下版本信息,果断OK~
<center>没啥可说的,等待,不过分分钟搞定~
<center>到此,基本配置已完成,但是如何查验NDK是否成功安装了呢?
配置下环境变量瞅瞅呗~
配置环境变量
复制NDK地址,按如下图示进行操作即可。
cmd直接输入ndk-build回车
<center>在这里吐槽下LZ之前遇到的坑。
LZ目录习惯命名为HLQWorkSofe(Android) or HLQWorkSofe(Java),在之前从来没有出现过问题,但是在NDK时候,却怎么也不行,最后无奈下只能把()去掉。
问题截图如下:
<center><font color=#FF0000>搞得LZ都快放弃了,最后一试,好了,给我郁闷的,大家注意啊~
创建个项目玩一玩
创建项目时,记得勾选下面的Include C++ support哦~
导入C++支持库,不然没法玩哈~
<center>
一路next下之后,关于最后一步选择时,在此作特殊说明下。
<center>C++ Standard:使用下拉列表选择您希望使用哪种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置;
Exceptions Support:如果您希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake;
Runtime Type Information Support:如果您希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
点击Finish之后,我们稍等片刻~
查看默认生成例子
<center>下面我们一起来探究下官方提供的例子,看看从例子中我们能得知什么对我们有用的信息。
从上图可看到,和以前唯一不同的就是多出了cpp目录以及External Build Files俩个目录,那么这俩个都有什么用呢?一起来看看。
cpp 目录存放所有 native code 的地方,包括源码,头文件,预编译项目等。对于新项目,Android Studio 创建了一个 C++ 模板文件:native-lib.cpp,并且将该文件放到了你的 app 模块的 src/main/cpp/ 目录下。这份模板代码提供了一个简答的 C++ 函数:stringFromJNI(),该函数返回一个字符串:”Hello from C++”
External Build Files 目录是存放 CMake 或 ndk-build 构建脚本的地方。有点类似于 build.gradle 文件告诉 Gradle 如何编译你的 APP 一样,CMake 和 ndk-build 也需要一个脚本来告知如何编译你的 native library。对于一个新的项目,Android Studio 创建了一个 CMake 脚本:CMakeLists.txt,并且将其放到了你的 module 的根目录下
首先还是来看看代码吧,比较直观也比较好理解。
1.查看activity代码
package cn.hlq.hlqjnipro;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
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();
}
首先static静态块去加载so库,本地编写native方法,调用方法输出内容。
2.查看native-lib.cpp代码
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_hlq_hlqjnipro_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
简单可以看到,首先定义hello变量,之后return。
3.查看CMakeLists.txt
简单的翻译了下,如有不正之处欢迎指出`
# 使用Android Studio使用CMake的更多信息,阅读文档:https://d.android.com/studio/projects/add-native-code.html
# 设置CMake的最低版本构建本机所需库
cmake_minimum_required(VERSION 3.4.1)
# 创建和名称库,使它是静态的或共享,并提供了相对路径的源代码
# 可以定义多个图书馆,CMake将为你构建这些内容
# Gradle自动与你的APK包共享库
add_library( # 设置库名称
native-lib
# 集库作为一个共享库
SHARED
# 提供了一个相对路径你的源文件
src/main/cpp/native-lib.cpp )
# 搜索指定预先构建的库和存储路径变量。因为CMake包括系统库搜索路径中默认情况下,只需要指定想添加公共NDK库的名称,在CMake验证库之前存在完成构建
find_library( # 设置path变量的名称
log-lib
# 在CMake定位前指定的NDK库名称
log )
# 指定库CMake应该链接到目标库中,可以链接多个库,比如定义库,构建脚本,预先构建的第三方库或者系统库
target_link_libraries( # 指定目标库
native-lib
# 目标库到日志库的链接 包含在NDK
${log-lib} )
一开始还觉的差不多点呢,结果看到这里越来越蒙圈,还是先运行一下,看看结果吧。
按照刚才的简单分析,这个demo输出的结果应该为:Hello from C++ 。一起来验证一下呗~
<center>嗯,确实,输出了,有些似懂非懂的,继续研究~
经过查询,发现一个之前从未关注的内容,那就是从编译到运行示例 APP 的流程到底是怎样呢,如下:
- Gradle 调用外部构建脚本,也就是 CMakeLists.txt;
- CMake 会根据构建脚本的指令去编译一个 C++ 源文件,也就是 native-lib.cpp,并将编译后的产物扔进共享对象库中,并将其命名为 libnative-lib.so,然后 Gradle 将其打包到 APK 中;
- 在运行期间,APP 的 MainActivity 会调用 System.loadLibrary() 方法,加载 native library。而这个库的原生函数,stringFromJNI(),就可以为 APP 所用了;
- MainActivity.onCreate() 方法会调用 stringFromJNI(),然后返回 “Hello from C++”,并更新 TextView 的显示;
注意:Instant Run 并不兼容使用了 native code 的项目。Android Studio 会自动禁止 Instant Run 功能。
如果你想验证一下 Gradle 是否将 native library 打包进了 APK,你可以使用 APK Analyzer:
选择 Build > Analyze APK。
从 app/build/outputs/apk/ 路径中选择 APK,并点击 OK。
如下图,在 APK Analyzer 窗口中,选择 lib/<ABI>/,你就可以看见 libnative-lib.so
转自:http://blog.csdn.net/wl9739/article/details/52607010
仿造demo来一下,首先布局中新增一个TextView,activity中编写一个native方法,调用native方法并输出结果,如下:
TextView tv1 = (TextView) findViewById(R.id.sample_text_1);
tv1.setText(hlqFromJNI());
public native String hlqFromJNI();
在cpp中这么玩下:
#include <jni.h>
#include <string>
extern "C" {
JNIEXPORT jstring JNICALL
Java_cn_hlq_hlqjnipro_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
JNIEXPORT jstring JNICALL
Java_cn_hlq_hlqjnipro_MainActivity_hlqFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "真恶心啊 Fuck";
return env->NewStringUTF(hello.c_str());
}
}
大家有没有注意到Java_cn_hlq_hlqjnipro_MainActivity_hlqFromJNI这个名称,方法名必须为Java_包名全路径_方法名
运行结果如下:
<center>一般情况下,Gradle 会将你的本地库构建成 .so 文件,然后将其打包到你的 APK 中。如果你想 Gradle 构建并打包某个特定的 ABI 。你可以在你的 module 层级的 build.gradle 文件中使用 ndk.abiFilters 标签来指定他们:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {...}
// or ndkBuild {...}
}
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a',
'arm64-v8a'
}
}
buildTypes {...}
externalNativeBuild {...}
}
提供给别人使用
ReBuild Project之后,将生成好的so库提供有需要的人即可,不过要记得调用的时候包名要一致,不然会报一个找不到so的问题。
LZ简单写了一个测试demo,如下图:
<center>运行结果如下:
<center>比较恶心的就是包名这块,等以后再看看有没有好的解决办法吧。
拓展APP构建流程
查看官方文档中,突然发现有关构建流程内容,一起了解下,如下:
构建流程涉及许多将您的项目转换成 Android 应用软件包 (APK)的工具和流程。构建流程非常灵活,因此了解它的一些底层工作原理会很有帮助。
典型 Android 应用模块的构建流程图如下:
典型 Android 应用模块的构建流程通常依循下列步骤:
1.编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括运行在 Android 设备上的字节码),将所有其他内容转换成已编译资源;
2.APK 打包器将 DEX 文件和已编译资源合并成单个 APK。不过,必须先签署 APK,才能将应用安装并部署到 Android 设备上;
3.APK 打包器使用调试或发布密钥库签署您的 APK:
a. 如果您构建的是调试版本的应用(即专用于测试和分析的应用),打包器会使用调试密钥库签署您的应用。Android Studio 自动使用调试密钥库配置新项目;
b. 如果您构建的是打算向外发布的发布版本应用,打包器会使用发布密钥库签署您的应用
4.在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时的内存占用
构建流程结束时,您将获得可用来进行部署、测试的调试 APK,或者可用来发布给外部用户的发布 APK。
参考文献
- http://baike.baidu.com/link?url=QrA-HKewzfPKP5UejCXH8JhE4yQs5MhwG6EBYI8imO9k8zYIlM0h2DYffNSqdK8dG6LZLfT0dK5ocK2NEsOUJq ;
- http://blog.csdn.net/tongseng/article/details/53005123;
- http://www.cnblogs.com/bastard/archive/2012/05/19/2508913.html;
- http://blog.csdn.net/baidu_26352053/article/details/53931045;
- https://developer.android.google.cn/ndk/guides/concepts.html;
- https://developer.android.google.cn/studio/build/index.html;
- http://blog.csdn.net/wl9739/article/details/52607010
结束语
这篇文章写的够揪心,个人感觉乱糟糟的,实际学习的时候也是乱糟糟的,几乎都是靠着各种搜索,况且还有一些不靠谱的博文误导或者说是版本有些老。
望观看者海涵`有问题及时沟通。