Android底层笔记:APP通过JNI调用动态库.so

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android底层笔记:APP通过JNI调用动态库.so相关的知识,希望对你有一定的参考价值。

开发环境:

  1. 平板电脑:FSPAD-733,原理上来说任何支持安卓的开发板都可以;
  2. eclipse:使用的是iTOP-4412开发板提供的eclipse安卓开发包;
  3. Ubuntu:使用的是FSPAD-733虚拟机开发环境,原理上来说任何开发包提供的虚拟机环境都是可以的。

加载库名,然后系统自动到库目录下找.so动态库

目录/库文件名

loadLibrary

技术分享

? ? ?

du -mh tags

androidL/art/

vi -t Runtim_nativeLoad

? ? ?

javah -jni Hello 生成头文件

JNINativeMethod methods[]就相当于在库中的函数集合

gcc -shared -fPIC native.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o liblednative.so

? ? ?

? ? ?

java Hello 执行Java可执行程序

export LD_LIBRARY_PATH=.

java Hello

java中的编码格式和C里面是不一样的,因为Java中是UTF-8能支持所有编码?而C中是Unicode编码,如何找到编码转换函数呢?搜JNI编程文档嘛,可以搜RegisterNative嘛,可以直接搜,或者想想在哪个步骤中会涉及这个东西。convert string 如果实在不知道搜什么,就往下翻吧。

? ? ?

如何把单个Java源文件放到eclipse里面结构里面

我们在eclipse里面做一个app,也就是在MainActivity.java里,你当然是造一个按钮啦

点击一个按钮就调用一个函数嘛ioctl

android studio和eclipse功能几乎一样啊,为什么以为差别很大呢?

? ? ?

private Button button = null;

button = (Button)findViewById(R.id....);

ledon = !ledon;

button.setText("led on");

? ? ?

可以在文件管理器里添加一个类包目录Le,然后把lednative.java拷贝进去,然后在eclipse里面刷新,项目会自动添加文件夹

package com.example.Led;

public class lednative {

static {

system.loadLibrary("lednative");

}

public native int ledopen();

publc

}

NDK

因为一切皆对象嘛,所以你总是在创建类,然后new对象,然后用对象调用方法来实现功能,而方法又调用其他代码,比如JNI代码啦

技术分享

把native.c编译生成动态库啊

arm-linux-gnueabi-gcc -shared -fPIC .../include -nostdlib 这里是非标库路径 -o liblednative.so

find -name libc.so

技术分享

? ? ?

技术分享

file liblednative.so 查看交叉编译器是否生成了arm格式的库,拷贝到windowsde fastboot目录,上传(打开一键烧写工具)到/system/lib目录,adb push liblednative.so /system/lib/,如果没有权限则mount -o remount /dev/block/by-name/system /system也就是arm平台目录

? ? ?

最后连接平板继续调试,看LogCat是否打印出错误信息

好吧,又出错啦:

技术分享

因为虽然加载了我们的本地动态库,但是本地动态库又要调用C库libc.so.6,这什么鬼东西?

那就到Android源码里面找啊 find -name libc.so.6

技术分享

首先明白lib.so是非标准库,然后搜GCC 使用非标准库 -nostdlib 不使用标准库,使用非标嘛

? ? ?

【学习方法】从成功范例里面学习,对于编译命令选项这些,一定要理解每个选项参数的意义,你可以把参数删掉,然后对照学习。便可以理解了。只管写代码啊,要动手。

? ? ?

--------------------首先我们在activity中创建一个button--修改button属性:

-------------------------------------------------------------------------------------------------------------------------------------------------------------

技术分享

--------------------接着我们在主程序中引用这个button,注意定义在类里面,而不是OnCreate函数里面哦:

技术分享

--------------------接着我们初始化这个button对象,管理我们创建的那个button:

技术分享

接着调用button的按键按下监听事件接口方法,在方法里面调用按下处理函数(匿名类函数(注意格式:是在小括号里面实现类的函数体的)哦):

技术分享

--------------------接着填充按键处理方法(技巧:new后面空一格,然后再按Alt + /可以自动填充):

技术分享

这里的ledon是一个状态指示灯,其实就指示了led硬件的状态啦!

注意要添加一个ledon的数据域field

技术分享

接下来我么简单调试一下这个demo:

  1. 插上平板电脑
  2. 点击运行应用
  3. 测试有效

--------------------接下啦放大招了,我们来通过JNI来调用C来访问硬件试试看,这里的C被封装成了C动态库 来直接和内核打交道了:

这里和C打交道的中间层(下层只认识上层,而不认识上上层,也就是你理解的时候只要每次在两个层之间就可以了)就是JNI,而JNI我们封装成一个类,在这个类里面我们加载C库,然后声明C库里面可以调用的方法

然后在主程序里面声明这个JNI类就好啦!

--------------------我们需要平板里面有动态库,然后应用程序APP去调用它,好的首先要创建一个动态库libled.so,这个库说白了就是C程序,只不过打包成了一个库的格式而已

? ? ?

问题层出不穷,主要出在依赖上而且Android系统有他自己实现,有时接口名字是很像的(其实本质就是接口名啦,然后上层又封装了而已而已):

  1. 在native.c
  2. JNI的库C程序的类名在eclipse里面调用的时候要写全名哦com/example/Led/lednative
  3. 如果重新生成C库最好把原来的删除了先,ll liblednative.so 查看日期
  4. 因为安卓没有终端,所以不能用printf和scanf,要用JNI的那种打印函数哦:
    1. grep "android_log_print" * ./* -nR
    2. 技术分享

      ? ? ?

    3. 技术分享
    4. logcat | grep led
    5. __android_log_print 替换成 ALOGI

--------------------我们先在eclipse里面写一个,JNI的调用类LedJNICall.java吧

我们的MainActivity.java是在package com.example.hello;包(文件夹)下面,我们再创建(如果跨包创建对象需要import包到时候)一个包com.example.led,然后把LedJNICall.java放到里面,不太清楚把LedJNICall.java也放到com.example.hello包里面可不可以?

在src上右键新建Class即可:

技术分享

? ? ?

技术分享

--------------------接着在这个里面我们加载C语言写的动态库,加载后,我们再调用这个动态库里面的函数,这些函数都是用C语言写的,所以可以做一些Java语言无法做的事情:

技术分享

--------------------好的,写完了LedJNICall.java,看一下有没有警告之类,然后Run As Android Application,好像没有反应啊:主活动MainActivity.java还没有调用LedJNICall.java这个类的方法呢!下面调用一下:

note:JNI接口类这里其实是个方法类,我们要它的目的是为了调用里面的方法,所以这里new类的对象的时候可以就放在主类的函数里面,哪里开始调用方法我们就在哪里开始new对象:

技术分享

--------------------这里的led操作其实就是相当于一个led驱动啊,所以访问led的时候,第一步是打开led,而不是操作led:

技术分享

--------------------打开设备后,就是根据具体的应用需求来操作led了,通用框架接口就是ledioctl,测试方法:因为我们这里是测试用的,所以在ledioctl里面用的是打印提示操作而非真的硬件操作:

技术分享

note:很显然么这里的ledioctl()就相当于一个驱动,而驱动是可以统治多个设备的,所以我们自然要选择一个设备,选择方法就是传输一个ID,然后控制动作也是通过参数传递的。

--------------------好吧,调试后出错,因为我们还没有加载C动态库,所以现在创建C动态库,我们在Ubuntu主机的Android目录下测试:

--------------------我们先编写一个Java的JNI文件调用C库:

技术分享

我们编译一下它javac LedJNICall.java得到Java的可执行程序文件:LedJNICall.class

运行一下java LedJNICall,运行出现异常:

技术分享

--------------------提示没有在java.library.path路径下找到lednative库文件:当然找不到了,因为我们还没有创建这个库,所以JNI无法完成调用,所以接下来就是写库文件lednative.c(note:下面这个程序中有错误,会在下面逐步修改):

技术分享

接着很自然地我们要编译这个lednative.c,但是得到错误:

技术分享

看看这个提示,说没有找到头文件jni.h,stdio.h能被编译器找到,是因为stdio.h所在的头目录/usr/include/已经被gcc编译器包含了,所以没有报错,现在jni.h所在的目录没有被配置,所以现在配置一下编译:

gcc -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include lednative.c

但是依然提示出错,因为库源文件是不包含main函数的,所以gcc链接的时候会报错:

技术分享

解决方法是编译成库就好了,编译方法是添加必要的选项然后让输出文件为动态链接库格式.so:

gcc -shared -fPIC lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o lednative.so

技术分享

好的,现在我们生成了lednative.so文件,接下来想到是,运行Java程序LedJNICall.class试试看java LedJNICall:

技术分享

好吧,又出错了,正如编译要添加头文件的目录,运行程序当然也要添加动态库的目录,道理是一样的 export LD_LIBRARY_PATH=.

技术分享

设置了链接库路径,结果还是错了,此时有两种情况,一是路径错了,而是路径里面的库错了。我当时没想到是什么情况,然后谷歌了一下,突然想起库文件名是有一定规范的:

gcc -shared -fPIC lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o liblednative.so

技术分享

好的,正确的库文件名生成成功:

技术分享

然后运行一下看看效果:

技术分享

又出错了,看错误提示,应该是本地方法出问题了,检查了好几次,终于发现问题:

技术分享

好的,把上面的错误改正一下在编译然后运行:

还是出错,经过检查总算对了:

技术分享

好的,终于运行成功了:

技术分享

? ? ?

--------------------至此,JNI调用生成的C动态库在Ubuntu主机上测试成功,现在该一直到eclipse上去了,让我们的APP应用程序通过JNI调用C动态库里面的函数ledon和ledioctl,怎么办呢?

我们应该把这个liblednative.so放到我们的平板上的Java库目录里面去,但是平板上的Java库目录是哪个?

Java中有一个函数可以获得java.library.path变量值:String javaLibraryPath = System.getProperty("java.library.path");

技术分享

上面的代码没有测试出来,先直接说答案吧,是/system/lib/ 和 /vendor/lib/两个目录

在你的代码里使用Log.i, Log.e, Log.w, Log.v, Log.d这几个函数可以输入log到Logcat

i 普通信息

e 错误信息

w 警告作息

v 详细信息

d 调试信息

分几个函数主要是为了给log信息分类。另外这些函数的第一个参数是一个字符型tag标志,也是用于给log分类的,第二个参数是你要输出的日志内容。

连接adb,先把动态库文件拷贝到fastboot目录下面去,然后把动态库推送到平板的目录里面去,我们来推一下:

技术分享

好的推送成功,推送步骤如截图所示,其中有一步显示 /system/lib/ 是只读目录,然后我们 adb remount 了一下就可以拥有读写权限了。我不太知道原理,不过 remount 的意思是把设备重新挂载到 /system 目录上去,可以这样理解,操作系统有一个目录 /system 目录,这个目录是只读的权限,但是访问这个目录其实访问的是挂载在它上面的设备,如果挂载的时候以读写的方式挂载,那么就可以访问这个设备而忽略 /system 目录本身的权限属性。

上面这是一种临时生效的方法,也就是系统重启后又不可以了。还有一种永久生效的方法,原理其实是一样的:

暂时忘了,就是修改那个存储设备挂载文件,然后重新编译系统再烧写到平板上。

--------------------好了,现在平板上已经有了动态库了,再运行一下APP看看:

技术分享

错误信息说动态库是64位的,而我们ARM要32位的,所以重新编译生成一下动态库:

gcc -shared -fPIC lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -m32 -o liblednative.so

编译之前,手残了,把lednative.c给删除了,没办法只能重新输入一遍,看了上git还是有必要的。

好的,把重新编译后的32位动态库推送到平板上,还是出错:

技术分享

错误信息说动态库的目标机器出错,所以估计是架构错了,因为我们是在x86机器上编译的,现在转移到ARM上,所以要用交叉编译工具才行啊,-m32去掉:

arm-linux-gnueabi-gcc -shared -fPIC lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o liblednative.so

技术分享

提示交叉编译工具链没有找到,那么这个工具链肯定是存在的,因为编译uboot和内核是也是用的这个编译器,所以怎么找呢?所以想到了,在lichee目录下全局搜索: grep "arm-linux-gnueabi-" * ./* -nR

功夫不负有心人,找到了这个命令所在的目录,所以接下来只要把这个目录添加到环境变量里面就可以了,所以添加到 /etc/profile 中,这个对当前用户有效,然后 source /etc/profile 一下:

~/fspad-733/lichee/brandy/gcc-linaro/bin/

技术分享

但是这样写出现问题了:

技术分享

环境变量丢失了:

只能重新配置一下,这个/etc/profile好像是配置的当前终端,而不是当前用户啊:

export PATH=/home/linux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games

[email protected]:~/fspad-733/lichee/brandy/gcc-linaro/bin:~/fspad-733/lichee/brandy/gcc-linaro/bin/

新开一个终端,输入:

export PATH=$PATH:~/fspad-733/lichee/brandy/gcc-linaro/bin/

技术分享

重新编译动态库文件:

arm-linux-gnueabi-gcc -shared -fPIC lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o liblednative.so

技术分享

又提示错误了,这个是什么意思?无法找到 crti.o 和 crtbegins.o,这两个好像是标准C库提供的链接文件,所以猜想,是因为调用了非安卓有的函数,这里是ARM编译器,所以可能会调用不同的C库,先把printf注释掉吧:

还是不行,那会是什么原因呢?原因是(不是我想出来的,是老师讲的):交叉编译器无法使用标准C库(C库的源代码如何查看?标准C库libc.so.6非标准C库,下载标准C库源码文件sudo apt-get source libc6-dev),既然无法使用标准C库,那么就让GCC不使用标准C库来编译吧:-nostdlib

技术分享

OK!编译成功,再次下载到板子上去测试:

技术分享

好吧,又出错了:

技术分享

错误信息显示没有"puts",可是动态库里面我没有使用"puts"啊,只使用了"printf",看来printf被优化成了"puts",我是这样理解的。为什么找不到,因为没有啊,安卓系统里的非标C库没有这个系统调用,所以不能用这种方式打印,先把那个printf注释掉再说吧:

技术分享

又出错了,由于错误信息最上面的是错误最新发生的地方,幸好我们有出错检查代码,所以检测到了一个错误返回JNI_ERR,但是我们库源文件里面有两个地方都是返回的同一个JNI_ERR,根本无法定位啊,所以改下源代码吧,把返回值改一下:

技术分享

好的,编译-加载动态库-eclipse调试,终于可以看到哪里错了:

技术分享

可见是调用FindClass函数出错了,常见的出错原因:

  1. 函数名写错了:不是
  2. 确实没找打:明明有这个类名啊,所以不是
  3. 传参错了:那么应该就是这个原因了,传参确实可能是错的,Android编程里面类名的本质是包名加类名

所以我们把类改下:

技术分享

编译-加载动态库-eclipse调试,又错了:

技术分享

很好,还给出了错误提示,也就是类名写错了,/是linux系统下的目录分隔符,和url的类似:

技术分享

再次编译-加载动态库-eclipse调试,又错了:

技术分享

这个错误我是真懵逼了,不过幸好有老师的标准编译代码做参考,这里的编译选项缺少了非标准库目录:

查找方法是 grep "libc.so" * ./* -nR,错了,我们要搜的是文件而不是文件内容,所以是用 find -name libc.so

技术分享

我们选择ndk目录下的这些基于特定架构的非标准C库文件,因为我们的平板上就包含了这些文件,所以编译的时候也要根据这些非标准库来实现编译,我们选择平台版本最高的那个版本:

? ? ?

arm-linux-gnueabi-gcc -shared -fPIC -nostdlib ~/fspad-733/androidL/prebuilts/ndk/9/platforms/android-19/arch-arm/usr/lib/libc.so lednative.c -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -o liblednative.so

但是编译后,还是同样的错误:这怎么解决?

技术分享

好吧,仔细看下出错信息,发现有222,说明找类错误了,怎么会找类又错了?盯着eclipse看了半天,发现类名错了,应该是:

技术分享

好了,这次测试程序终于成功,没有出错信息输出了。

技术分享

? ??

以上是关于Android底层笔记:APP通过JNI调用动态库.so的主要内容,如果未能解决你的问题,请参考以下文章

Android通过jni调用本地c/c++接口方法总结

Android JNI开发

Android项目中JNI技术生成并调用.so动态库实现详解

android jni控制gpio (rk3288)

Android Studio调用第三方动态库

Android Studio调用第三方动态库