Android Framwork开发过程中,经常会遇到新增接口的需求,下面将介绍基本上所有需要用到的接口开发流程。
一、JNI接口
在系统开发接口过程中,很多情况需要通过JNI接口的方式调用一些库或者驱动,通过Android studio的方式构建native工程比较简单,下面主要介绍在framework层如何自定义jni接口。
JNI的原理参考:https://skytoby.github.io/2020/Android%20JNI%20%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/
1.1 源码编译so库
在 vendor 目录下创建 skytoby目录,我们把代码放到 testjni 里面。
测试jni接口源码文件,里面有一个jni方法还有一个带回调的jni方法例子
1 |
|
testjni目录下增加编译文件文件Android.bp
1 | cc_library_shared { |
在testjni目录下下,执行mm,执行成功后会在对应的out目前下的system\lib64有一个libtestjni.so文件生成,说明编译成功,那如何让Java层的Framework可以调用到这个库里面的方法,下面将介绍。
1.2 so库导入系统
将库编译进系统,只需要在device目前下的mk文件加上如下代码即可
1 | PRODUCT_PACKAGES += libtestjni |
1.3 Java jni调用
加载so,编写native函数
1 | package com.skytoby; |
回调接口
1 | package com.skytoby; |
测试代码
1 | JniTest jniTest = new JniTest(); |
在framework代码中加上测试代码,可以正常看到打印。
二、Jave服务接口
2.1 使用 aidl 定义服务接口
首先我们得定义我们的服务名是什么,提供什么样的接口。 在这个例子里面,我们的服务就叫 HelloService,它提供了下接口:
- void hello(String name) //播放指定路径的音频文件
在 frameworks/base/core/java/android 目录下创建 pure 目录,我们把代码放到 pure 里面。 pure 目录下增加中文件 IHelloService.aidl
1 | package android.pure; |
在 frameworks/base/Android.bp framework-defaults 模块中添加我们刚刚加的 aidl 文件
1 | "core/java/android/pure/IHelloService.aidl", |
然后进入 framework/base 目录执行 mm -j 命令编译 framework.jar 模块。 编译成功后,会在 out/soong/.intermediates/frameworks/base/framework/android_common/gen/aidl/frameworks/base/core/java/android/pure 目录生成 IHelloService.java 这个文件。 这个文件封装了 binder 通信的相关细节,有兴趣的同学可以去了解一下。
2.2 实现接口
在 frameworks/base/services/core/java/com/android/server 目录下新建 HelloService.java 文件:
1 | package com.android.server; |
2.3 将服务添加到 ServiceManager
修改 frameworks/base/services/java/com/android/server/SystemServer.java 文件,在 startOtherServices 方法里面增加以下代码:
1 | // add hello service |
至此服务算是添加完了,我们来编译运行一下,发现系统起不来了,看下错误 log
1 | 12-20 23:46:47.308 3521 3521 I SystemServer: HelloService |
从 SELinux : avc: denied { add } for service=HelloService
这个信息来看,是因为我们没有添加 SELinux 规则,什么是 SELinux 这里就不展开了,同学们可以自己去查下资料。
2.4 selinux 规则设置
Android 10 的 selinux 规则是放在 system/sepolicy 目录下的。 但 Android 每个版本 selinux 的添加规则多多少少是有些变化的,我们要怎么把这个新服务的 SELinux 规则给加上呢, 我们可以参考现有的系统服务的规则去添加,这里参考的是 network_time_update_service 服务:
1 | cd system/sepolicy |
涉及到的文件很多,有部分文件是不需要修改的,我们先把找到的所有 service.te 和 service_contexts 都参考 network_time_update_service 加上 HelloService 的配置。
service_contexts 加上
1 | HelloService u:object_r:HelloService:s0 |
service.te 加上
1 | type HelloService, system_server_service, service_manager_type; |
2.5 编译验证
完成以上各步骤的修改后,我们就可以先来验证一下,服务是否添加成功了。 先 make api-stubs-docs-update-current-api -j20
更新一下 api 接口,再整编系统。 运行虚拟机,执行 service list
看看有没有 HelloService 服务。
1 | pure:/ # service list | grep HelloService |
2.6 客户端调用
我们创建完服务端之后,还要考虑怎么提供 api 接口给到 client 使用。我们分为两种情况来讨论。
2.6.1 系统源码中使用
假如我们上面添加的服务是给在系统源码中编译的模块使用的,那就简单了。比如一个 apk ,我们在系统源码中创建一个 apk 模块。目录结构如下:
1 | skytoby10/device/skytoby/pure/models$ tree PureSettings/ -pc:~/source/android- |
Android.bp:
1 | android_app { |
其中 platform_apis 要设置为 true,不然没法调用我们新加的服务和 ServiceManager 。
AndroidManifest.xml:
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout/activity_main.xml:
1 | <?xml version="1.0" encoding="utf-8"?> |
src/com/pure/settings/MainActivity.java:
1 | package com.pure.settings; |
把模块添加到系统, pure.mk:
1 | PRODUCT_PACKAGES += PureSettings |
编译运行,闪退了,看下错误日志,发现:
1 | 12-21 23:55:32.970 1526 1526 E SELinux : avc: denied { find } for service=HelloService pid=4266 uid=10102 scontext=u:r:platform_app:s0:c512,c768 tcontext=u:object_r:HelloService:s0 tclass=service_manager permissive=0 |
看起来是还有 selinux 权限需要添加。具体分析如下 缺少什么权限:{find}权限, 谁缺少权限:scontext=u:r:platform_app:s0:c512,c768 对哪个文件缺少权限:tcontext=u:object_r:HelloService:s0 什么类型的文件:tclass=service_manager
完整的意思: platform_app 缺少 service_manager 类型的 HelloService 的 find 权限。
根据以上分析,我们就可以找到解决方案: 在 platform_app.te 中添加 HelloService 的 find 权限即可。
1 | allow platform_app HelloService:service_manager find;allow platform_app HelloService:service_manager find; |
我们查找一下有哪些 platform_app.te 文件:
1 | skytoby10/system/sepolicy$ find -name platform_app.te -pc:~/source/android- |
把找到的所有 private/platform_app.te 都加上后,再重新编译运行发现不会挂了,看下 logcat:
1 | 130|pure:/ # logcat -c;logcat -s "HelloService:V" "PureSettings:V" |
的确调用到了我们添加的服务。
2.6.2 非系统源码中使用
前面实现了在 Android 系统源码中调用新添加服务的方法,但在实际项目中很多情况是在非系统源码环境下使用的。这种情况我们一般是再封装一层接口出来。 调用的层级为: apk –> HelloApi –> HelloService 我们按以下目录结构创建 HelloApi 模块:
1 | skytoby10/device/skytoby/pure/models$ tree HelloApi/ -pc:~/source/android- |
Android.bp:
1 | java_library { |
HelloManager.java:
1 | package com.pure.api; |
然后到 HelloApi 目录 mm 编译模块,得到 out/target/common/obj/JAVA_LIBRARIES/com.pure.api_intermediates/classes.jar 文件。 我们把这个 classes.jar 文件,复制到用 android-studio 创建的 apk 项目 的 app/libs 目录下,改名为 com.pure.api.jar 。 右键单击 com.pure.api.jar 选择 add as library, 然后我们就可以在 apk 的代码中使用 com.pure.api.jar 的接口了:
1 | ... |
安装 apk 到虚拟机上运行,又遇到了 SELinux : avc: denied 错误,这次的错误是
1 | E SELinux : avc: denied { find } for service=HelloService pid=4266 uid=10102 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:HelloService:s0 tclass=service_manager permissive=0 |
跟上面的错误基本上是一样的,只不过是把 platform_app 换成了 untrusted_app 而已。platform_app 表示有系统签名的 apk, untrusted_app 表示没有系统签名的 apk。 我们按同样的思路添加完 selinux 的权限后,重新编译运行虚拟机,就可以正常调用服务的接口了。
2.7 添加服务回调
2.7.1 使用 aidl 定义callback接口
在 frameworks/base/core/java/android/pure 目录下创建 ICallback.aidl
1 | package android.pure; |
在 frameworks/base/Android.bp framework-defaults 模块中添加我们刚刚加的 aidl 文件
1 | "core/java/android/pure/ICallback.aidl", |
然后进入 framework/base 目录执行 mm -j 命令编译 framework.jar 模块。 编译成功后,会在 out/soong/.intermediates/frameworks/base/framework/android_common/gen/aidl/frameworks/base/core/java/android/pure 目录生成 ICallback.java 这个文件。
2.7.2 添加注册回调接口
修改 IHelloService.aidl 如下
1 | package android.pure; |
HelloService.java 修改如下:
1 | package com.android.server; |
我们把 client 传递过来的 callback 保存到 mClients 集合中,当 client 调用 hello 接口时, 遍历 mclients 把消息传递给 client。
2.7.3 client 注册回调接口
修改 PureSettings apk 中的 MainActivity
1 | package com.pure.settings; |
2.7.4 编译验证
整编系统,运行虚拟机,打开 PureSettings 测试,查看 logcat
1 | pure:/ # logcat -s HelloService:D PureSettings:D |
从 log 上看,client 收到了 service 传递过来的消息。
三、Native服务接口
上一小节我们学习了 java 层的系统服务,但有时候出于性能方面的考虑,有些服务是需要使用 c++ 来实现的,比如音视频编解码,图形绘制等。 Android 系统原生的 MediaPlayerService 和 SurfaceFlinger 就是使用 c++ 实现的 Native 系统服务。这一小节我们就来学习一下如何添加一个 HelloNativeService。
3.1 声明服务接口
我们先在 $device 目录下创建 HelloNativeService 目录
1 | cd device/skytoby/pure |
然后创建 service 的头文件 HelloNativeService.h
1 | #ifndef ANDROID_10_HELLONATIVESERVICE_H |
这里我们直接继承的是 BBinder, 而不是像其他教程一样又 BpInterface, 又 BnInterface 的,还把 BpInterface 跟 BnInterface 的实现放到同一个 cpp 文件中, 把 client 跟 server 的代码放到同一个文件中,个人感觉把简单的事情复杂化了。
3.2. 实现服务功能
创建文件 HelloNativeService.cpp
1 | #include <android/log.h> |
其中最重要的是 onTransact 函数的实现,这是一个回调函数,当 client 端通过 binder 调用 server 功能的时候, server 端的 onTransact 回调函数就会被调用。 各参数理解如下:
- code : 表示要执行的动作,类似Handler发送的Message的what。code指示了当前远程操作的命令,IBinder定义了像INTERFACE_TRANSACTION、PING_TRANSACTION 这样的几个通用命令。自己使用的命令的标识值需要在FIRST_CALL_TRANSACTION和LAST_CALL_TRANSACTION之间。
- request, reply : request 和 reply 参数相当于普通函数里的调用参数和返回值。Parcel类型是可以跨进程的数据。
- flag : 参数 flags 只有 0 和 FLAG_ONEWAY 两种。0 表示阻塞调用, FLAG_ONEWAY 表示异步调用。这个参数不需要我们在服务端处理, Binder 框架会自动处理。
3.3. 添加 main.cpp
前面我们定义了服务接口而已,这个服务也是需要依赖在某个进程才能运行的
1 | #include <binder/IServiceManager.h> |
以上都是样板代码,具体 ProcessState 的功能作用是什么这里就不展开了。
3.4 Android.bp
1 | cc_binary { |
3.5 selinux 规则设置
为了简单起见,我们直接修改系统默认的 sepolicy 规则路径了。
3.5.1 添加 HelloNativeService.te
system/sepolicy/private/HelloNativeService.te 和 system/sepolicy/prebuilts/api/29.0/private/HelloNativeService.te 文件内容保持一致
1 | type HelloNativeService, domain; |
3.5.2 修改 file_contexts
system/sepolicy/private/file_contexts 和 system/sepolicy/prebuilts/api/29.0/private/file_contexts 文件内容保持一致,添加以下配置
1 | /system/bin/HelloNativeService u:object_r:HelloNativeService_exec:s0 |
3.5.3 修改 service_contexts
system/sepolicy/private/service_contexts 和 system/sepolicy/prebuilts/api/29.0/private/service_contexts 文件内容保持一致,添加以下配置
1 | HelloNativeService u:object_r:HelloNativeService_service:s0 |
5.4 修改 service.te
system/sepolicy/private/service.te 和 system/sepolicy/prebuilts/api/29.0/private/service.te,添加以下配置
1 | type HelloNativeService_service, service_manager_type; |
3.6 开机自动运行
在 device/skytoby/pure 目录添加rc文件 init.ranchu.rc
1 | service HelloNativeService /system/bin/HelloNativeService |
在 pure.mk 中添加以下配置, 这个配置需要添加在 $(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_x86_64.mk) 之前。
1 | PRODUCT_COPY_FILES += device/skytoby/pure/init.ranchu.rc:root/init.ranchu.rc |
3.7 编写 client 调用服务
我们把测试代码跟 service 放在同一个目录,直接在 HelloNativeService 目录下创建 HelloNativeTest.cpp 文件:
1 | #include <binder/IServiceManager.h> |
Android.bp 中添加
1 | cc_binary { |
3.8 编译运行
经过以上步骤,我们的 native 服务和测试 demo 就完成了,目录结构如下:
1 | skytoby10/device/skytoby/pure/models$ tree HelloNativeService/ -pc:~/source/android- |
我们还需要把刚刚添加的这两个模块添加到 PRODUCT_PACKAGES
1 | PRODUCT_PACKAGES += HelloNativeService HelloNativeTest |
然后编译运行,查看服务列表,可以看到我们的 HelloNativeService 已经在运行状态了。 然后执行 HelloNativeTest,服务端正确返回了计算结果。 查看一下 logcat, HelloNativeService 也正确收到了 HelloNativeTest 传递过来的数据。
1 | skytoby10$ adb shell -pc:~/source/android- |
3.9 添加native服务回调
3.9.1 client 端更改
为了简单省事,我们直接在上一小节的 demo 基础上作修改, HelloNatvieTest.cpp 修改如下:
1 | #include <binder/IServiceManager.h> |
增加了一个 Callback 类,并且增加了一个 CMD_CONNECT 命令。 main 函数中创建了一个 Callback 对象,然后通过 writeStrongBinder 把这个对象传给 server。
3.9.2 server 端更改
HelloNativeService.h ,增加client变量修改如下:
1 | sp<IBinder> client; |
HelloNativeService.cpp 修改如下:
1 | #include <android/log.h> |
增加了一个 CMD_CONNECT 命令,通过 readStrongBinder 把 client 传送过来的 callback 对象给读取保存下来。后面需要回调客户端的时候只需要通过 client 这个 binder 就可以。 client binder 与 server binder 的一个差异是 client binder 对象不需要添加到系统服务里面去,而是通过 writeStrongBinder 直接传送给 server。
3.9.3 编译验证
1 | pure:/ # HelloNativeTest |
四、HIDL服务接口
Android 8.0 之后,google 为了解决 Android 版本碎片化问题, 推出了 treble 架构,参考 Android Treble架构解析。核心思想就是 vendor 分区和 system 分区的隔离。把厂商的更改限制在 vendor 分区,system 分区由 google 把控。system 分区需要访问 vendor 分区的话,需要通过 hidl 的形式来访问。 网上有很多介绍 hidl 的文章,但没有一篇是介绍得比较全面的,都会遗漏一些小细节,由于这样,那样的小细节,自己到处找资料,折腾了两天才把 hidl 的demo 跑起来。以下作详细的记录,手把手教你从零创建自己的 hidl 服务。关于什么是 hidl ,及为什么要用 hidl 的理论知识这里就不作介绍了,可以参考官网的介绍 : hidl概览
4.1 添加 hal 目录
网上的 demo 全部都是直接在 android/hardware/interfaces 目录下添加了, 但我觉得最好是在自己的 device 目录下添加比较好维护一点。
1 | mkdir -p device/skytoby/pure/models/hidl/tvserver/1.0 |
4.2 定义 hal 接口
在 device/skytoby/pure/models/hidl/tvserver/1.0 目录下创建文件 ITVServer.hal
1 | package device.skytoby.pure.tvserver@1.0; |
4.3 根据 .hal 自动生成 cpp 实现
1 | skytoby10$ PACKAGE=device.skytoby.pure.tvserver@1.0 -pc:~/source/android- |
执行完以上命令之后在 default 目录下生成了 TVServer.h 和 TVServer.cpp 这两个文件 我们需要对 TVServer.cpp 实现进行完善,只需要对我们定义的 hello 接口进行实现就行了,修改如下:
1 | Return<void> TVServer::hello(const hidl_string& name, hello_cb _hidl_cb) { |
我们还需要一个进程来容纳我们的服务,创建一个 main.cpp 文件
1 | #include <hidl/HidlTransportSupport.h> |
4.4 开机自动启动服务
在 tvserver/1.0/default 目录下创建 device.skytoby.pure.tvserver@1.0-service.rc 文件:
1 | service tvserver /vendor/bin/hw/device.skytoby.pure.tvserver@1.0-service |
4.5 生成 Android.bp
1 | hidl-gen -o $LOC -Landroidbp-impl -rdevice.skytoby.pure:device/skytoby/pure/models/hidl $PACKAGE |
在 Android 9.0 之后,查看自动生成的 Android.bp 文件,里面有些提示,说明自动生成的文件还需要稍作修改,我们修改如下:
1 | cc_binary { |
其中 device.skytoby.pure.tvserver@1.0-service.rc 文件会被安装到 /vendor/etc/init/ 目录。系统启动时会自动加载这个目录下的所有 rc 文件。
此时目录结构是这样的
1 | skytoby10/device/skytoby/pure/models/hidl$ tree tvserver/ -pc:~/source/android- |
4.6 自动生成 hal 接口的 Android.bp
这里需要用到 update-makefiles.sh 这个脚本,我们可以从 $(TOP)/hardware/interfaces 里面 copy 一份过来放到 device/skytoby/pure/models/hidl 目录,稍作修改即可
1 | #!/bin/bash |
然后在 Android 根目录执行
1 | ./device/skytoby/pure/models/hidl/update-makefiles.sh |
这个命令生成了 device/skytoby/pure/models/hidl/tvserver/1.0/Android.bp 这个文件:
1 | // This file is autogenerated by hidl-gen -Landroidbp. |
我们需要在这个文件最前面添加以下配置才行,不然会编译不过
1 | hidl_package_root { |
目前目录结构是这样的:
1 | skytoby10/device/skytoby/pure/models/hidl$ tree -pc:~/source/android- |
4.7 更新 current.txt
current.txt 记录了所有 hal 接口的 hash 值,接口有变化时,同时需要更新 current.txt 中的 hash 值 在 interfaces 目录下新建 current.txt 文件,随便写个 hash 值
1 | 123456 device.skytoby.pure.tvserver@1.0::ITVServer |
再执行一遍 update-makefiles.sh,这个时候就会发现提示 hash 值不正确了,同时会给出正确的 hash 值,我们把正确的 hash 值替换到 current.txt 即可。
4.8 模块编译
1 | mmm device/skytoby/pure/models/hidl/tvserver/1.0 |
编译完成后,可以发现 $(TARGET_OUT)/vendor/bin/hw 目录下生成了我们的服务 device.skytoby.pure.tvserver@1.0-service
4.9 更新 manifest.xml
复制 device/generic/goldfish/manifest.xml 到 device/skytoby/pure/manifest.xml manifest.xml 中加上
1 | <hal format="hidl"> |
pure/product_copy_files.mk 中加上:
1 | PRODUCT_COPY_FILES += \ |
4.10 添加到 PRODUCT_PACKAGES
修改 device.mk 增加
1 | PRODUCT_PACKAGES += \ |
4.11 添加 selinux 规则
系统原生的 selinux 规则在 system/sepolicy 目录下, 我们可以直接修改里面的规则,但不推荐这么做,最好是在 device 目录下添加厂商自己的规则。 在 BoardConfig.mk 里面添加:
1 | BOARD_SEPOLICY_DIRS += device/skytoby/pure/sepolicy |
然后我们就可以在 pure/sepolicy 目录下添加我们的 selinux 规则了。
新增 tvserver.te
1 | type tvserver, domain; |
新增 file_contexts
1 | /vendor/bin/hw/device.skytoby.pure.tvserver@1.0-service u:object_r:tvserver_exec:s0 |
新增 hwservice.te
1 | type tvserver_hwservice, hwservice_manager_type; |
新增 hwservice_contexts
然后去编译整个系统, 运行,嘿,我们的服务跑起来了:
4.12 编写 client 调用服务
在 1.0 目录下增加 test 目录,新建 TVServerTest.cpp 文件
1 | #include <device/skytoby/pure/tvserver/1.0/ITVServer.h> |
新建 Android.bp
1 | cc_binary { |
在 device.mk 中加上
1 | PRODUCT_PACKAGES += \ |
然后整编译系统,运行:
1 | pure:/ # TVServerTest |
成功调用了 hidl 服务。
最终代码目录结构是这样的
1 | skytoby10/device/skytoby/pure/models/hidl$ tree -pc:~/source/android- |
经过了这么多的步骤终于完成了一个 hidl 的 hello world。 以后我们只要在这个框架上慢慢添加功能就行了。
网上大部分文章会忽略掉 current.txt, manifest.xml, selinux 及在 device/product 目录下添加模块的这些问题,导致自己跟着那些文章实现的时候,总是遇到各种问题,希望这篇记录能够帮助到想创建 hidl 服务的同学。
4.13 增加hidl服务回调
上一小节我们已经完整的跑起来了一个 hidl 的 hello demo,这个 demo 虽然非常简单,但已经包含了 hidl 的大部分知识点了,我们只要在这个框架的基础上不断的扩展功能即可。 上一小节的demo 调用是单向的,即只能由 client 调用 service 的功能,但实际项目中我们是经常需要在 service 里面通知 client 某个事件发生了,需要 client 进行处理。也就是双向调用,或者称为 callback。下面我们在上一小节的基础上增加一个回调功能。
4.13.1 增加回调 hal 接口
在 tvserver/1.0 目录下增加 ITVServerListener.hal 文件
1 | package device.skytoby.pure.tvserver@1.0; |
4.13.2 ITVServer.hal 增加回调注册接口
1 | package device.skytoby.pure.tvserver@1.0; |
4.13.3 更新 current.txt
current.txt 中增加 ITVServerListener 接口的 hash 值,先随便写一个,然后执行
1 | ./device/skytoby/pure/models/hidl/update-makefiles.sh |
会提示 hash 值不对,更新 current.txt 即可, 两个 hal 接口的 hash 值都更新之后,再执行 update-makefiles.sh
4.13.4 实现新增的 hal 接口
TVServer.h
1 | ... |
增加了 registerListener, unregisterListener 两个方法, 这两个方法的参数类型可以根据 hal 文件中声明的参数类型来推断。 hidl 与 c++ 的数据类型转换可以按下表来转换一下,给所有参数都加上 const 数据类型
hal 类型的参数,则使用 const sp& listener 这种智能指针的形式。 当然我们也可以使用 hidl-gen 来自动生成代码。参考上一小节的:根据 .hal 自动生成 cpp 实现。
TVServer.cpp
1 | #include <android/log.h> |
4.13.5 更新 manifest.xml
增加一个interface节点就行
1 | <hal format="hidl"> |
4.13.6 更新 client
TVServerTest.cpp
1 | #include <device/skytoby/pure/tvserver/1.0/ITVServer.h> |
4.13.7 编译验证
整编系统,执行
1 | pure:/ # TVServerTest |
可以看到收到了 onMessage 回调。
参考资料
http://qiushao.net/categories/Android%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8/