JNI与HIDL实现上层应用对内核节点的读写

前言设定

在进行安卓系统级应用开发的过程中,一些功能需要去调用或操作底层硬件或者系统级变量,而就现在掌握的技能来说上层通过java调用底层linux解耦度比较高的方式就是通过JNI抽象底层函数进行调用。在我去写一个练习项目的时候遇到了一些关于jni和hidl的问题,随着学习深入的过程,从java到C的流程也逐渐清晰。所以决定记录一下自己解决和未解决的问题以供之后研究或共同探讨。

整体设计

在一开始接触jni和hidl的时候,可以在网上找到一些形形色色的demo。但是问题主要存在于两方面:

  1. 在实现JNI和HIDL的demo过程中涉及到很多关于联合编译和动态编译的操作,而很多demo教程无法明确出操作的意义和实现具体流程。
  2. 找了无数个demo后终于写出了一个helloworld的demo后,不知道怎么去在项目中进行整合,换言之就是照猫画虎。这对于后期拉通整个逻辑是非常不利的。

本片博客只涉及拉通上下,jni和hidl的详细demo问题可以参照另外两篇独立的博客,即便已经实现了demo,我也建议去看一看jni和hidl那么写的意义究竟是什么:

下面是一张我认为表述非常清晰的图片,这张图是在一个github的开源项目中得到的

可以看到最左侧在源码进行编译的时候,上下层其实是编译成两个不同的镜像的:上层为system.img,底层为vendor.img。 如果直接在两个镜像的“域”中进行传参和调用无疑会出现无数的权限问题和空指针。HIDL就是为了解决上层去调用HAL层的一种技术思想。他对HAL层中对于底层device 的调用方法封装成调用接口,并把调用的部分作为系统服务进行启动。从上层的HIDLClient去访问server进行调用底层,这样的过程耦合度非常之低,并且方便上下层开发人员进行接口对接,并且在日后的演进过程中会极大地减少调试的时间成本和可以避免的bug量。

JNI

在jni的实现过程中,我们可以简单的分为以下步骤:

  1. 在java类中读library并声明native方法
  2. 通过javah编译类文件生成后缀为.h的头文件
  3. 创建.c/cpp文件来include生成的头文件并且实现里面的函数
  4. 通过CMake或者ndk-build工具打包成.so动态库

这个过程中可能会出现如下的疑问:

  • javah报错找不到指定类文件: 出现这种情况的原因大概率是因为路径不对,在我实现demo的过程中看了大约四篇博客,其中三篇的javah命令是有问题的并且没有做出任何解释,这也是很多人可能遇到的第一个问题,解决方式可以cd 到项目的java目录下然后引用类路径为完整包名+类名(不加后缀)
    javah -classpath . com.XXX.XXX.XXX.JNIDemo(你的类名)
    这样的命令执行方式是从java目录下开始读到com/XXX/XXX/XXX/JNIDemo

  • 为什么去生成so,so是什么: 因为我是java方向的开发,在刚开始接触编译动态库的时候是没有办法理解其意义的。但是细心地人会发现在通过Android.mk生成的过程中会指定生成的架构,具体如下:

    1、armeabi-v7a:第七代及以上的ARM处理器,2011年以后生产的大部分Android设备都使用。
    2、arm64-v8a:第8代、64位ARM处理器,很少设备,三星GalaxyS6是其中之一。
    3、armeabi:第5代、第6代的ARM处理器,早期的手机用的比较多。
    4、X86:平板、模拟器用得比较多。
    5、X86_64:64位得平板。

那么为什么要按照架构生成对应的so呢,联想到他的名字“动态库”相比大多数人都有一个初步的理解。动态库的生成其实可以类比于静态库的编译,在执行c文件的时候要先进行编译成可执行文件,动态库可以简单理解成在不同架构下编译出的可动态调用的执行文件,里面包含了打包的函数,生成so之后我们的jni调用过程就是一个完整的了,从native方法映射到so中的包名对应函数。

HIDL

hidl的demo有一个博客写的比较好,避免重复造轮子直接贴出一个链接:https://qiushao.net/2020/01/07/Android系统开发入门/11-添加hidl服务/

这个demo其实是可以直接在基础上进行完善并使用的,在server中直接去操作底层。而问题在于如何在jni中进行调用。如果你一切顺利完成了demo就可以看到现在client的函数已经可以调用到server并修改底层节点了。而在这个demo中client中的函数是没有专门用.h头文件进行定义的,所以我们可以在它的基础上手写一个.h头文件并在里面定义函数,在client中include头文件再做实现,这样我们就在他的基础上分离了函数的定义和实现。为什么要做这一步操作呢?这里其实就是很多人无法拉通jni和hidl逻辑的关键点。
我们要实现的无非就是要在jni的c文件中调用client中的函数,例如在client中有一个函数名为opensensor,那么我们可以在jni的C文件中include刚刚分离出client的头文件,就可以直接在jni的函数中调用opensensor函数,这就是我们选择分离函数定义和实现的原因。
但是这样的过程一定是有问题的,因为我们的java调用jni的过程实际上是调用了便移出的so库中的函数,但是现在经过改动调用了不存在于现有so
库中的函数opensensor,所以我们在需要头文件的基础上,还要对client进行动态编译生成so库,和jni的动态库一起编译才能保证jni到client到server是一个通的过程。

实现步骤

1. 将hidl client.c文件分离出.h 头文件
2. 编辑Android.bp动态编译client生成.so库
3. 在jni目录下新建文件夹prebuild,将hidl client生成的so库和头文件放入prebuild,在prebuild下新建Android.mk:
  LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
  LOCAL_MODULE := ClientTest
  LOCAL_SRC_FILES := libClientTest.so
  include $(PREBUILT_SHARED_LIBRARY)
4. 在jni目录下修改Android.mk:
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := ClientTest
LOCAL_SRC_FILES := ./prebuilt/libClientTest.so
LOCAL_EXPORT_C_INCLUDES := ./prebuilt
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := JNIDemo
LOCAL_SRC_FILES := jni.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_LDLIBS += -llog
LOCAL_SHARED_LIBRARIES := libClientTest
include $(BUILD_SHARED_LIBRARY)
include $(LOCAL_PATH/prebuilt/Android.mk)
5. 在jni目录下ndk-build进行编译,CMake没有了解过可以自己试一试,最后所需的so库就会像只编译jni一样生成在lib目录下

至此上下层已经可以顺利调用,如果出现其他bug可以自行定位并调整一下。对于所提到的内容有所疑问可以发送邮箱联系我,理解有偏颇的地方也欢迎交流指导。
邮箱: s947517134@163.com

posted @ 2021-08-11 16:44  冒蓝火的帅可可  阅读(1636)  评论(0)    收藏  举报
//线条