Android操作系统是一个复杂的生态系统,有时我们可能需要在系统级别添加自定义的Java类库。本文将指导你如何在Android中系统级加载自定义的JAR文件。
1. 问题描述
我是新增了个Framework binder demo
,这个demo包含(ClientDemo
和 ServerDemo
)可执行文件 ,需要依赖(/system/framework/ClientDemo.jar:/system/framework/ServerDemo.jar
)当我尝试在Android系统中直接启动可执行文件时,遇到以下错误:
ClassLoader referenced unknown path: /system/framework/ClientDemo.jar 2023-09-13 15:53:04.226 10765-10765 appproc pid-10765 E ERROR: could not find class 'com.ln28.frameworkBinder.ClientDemo' 2023-09-13 15:53:04.226 10765-10765 app_process pid-10765 A thread.cc:2361] No pending exception expected: java.lang.ClassNotFoundException: com.ln28.frameworkBinder.ClientDemo thread.cc:2361] (Throwable with empty stack trace) .............
这意味着Android系统在启动时没有加载我们的JAR文件?
2. 解决方案
2.1 完整调试过程
问题找到了,我想尝试反编译这个JAR确认文件是否正常 ,使用平常的反编译工具打开
然后查阅资料发现 ClientDemo.jar文件包含一个classes.dex,那么它实际上是一个DEX文件,不是一个常规的Java JAR文件。DEX文件是Android特有的,它包含为Dalvik或ART运行时优化的字节码。
要查看DEX文件中的类,需要使用到dexdump工具,使用它来列出DEX文件中的所有类:
rk3568_r:/ # dexdump -l plain /system/framework/ClientDemo.jar Processing '/system/framework/ClientDemo.jar'... Opened '/system/framework/ClientDemo.jar', DEX version '039' Class #0 - Class descriptor : 'Lcom/ln28/frameworkBinder/ClientDemo;' Access flags : 0x0001 (PUBLIC) Superclass : 'Ljava/lang/Object;' Interfaces - Static fields - Instance fields - Direct methods - #0 : (in Lcom/ln28/frameworkBinder/ClientDemo;) name : '<init>' type : '()V' access : 0x10001 (PUBLIC CONSTRUCTOR) code - registers : 1 ins : 1 outs : 1 insns size : 4 16-bit code units catches : (none) positions : 0x0000 line=7 locals : ........ Class #1 - Class descriptor : 'Lcom/ln28/frameworkBinder/IMyService;' Access flags : 0x0601 (PUBLIC INTERFACE ABSTRACT) Superclass : 'Ljava/lang/Object;' Interfaces - ........
这将列出DEX文件中的所有类和方法。
如果确定 .jar 文件是正确的,并且类确实存在,那么问题可能是与Android的类加载器或应用程序的构建和部署过程有关。
因为这个类是自定义的 怀疑可能没有被系统加载 那么该如何确认呢?
在Android系统启动时,BOOTCLASSPATH环境变量定义了系统类加载器应该加载哪些JAR文件。知道是环境变量就好办了 还好有些Linux基础 , 通过基操读出BOOTCLASSPATH环境变量 看到一堆jar定义 好家伙总算找到突破口了
rk3568_r:/ # echo $BOOTCLASSPATH /apex/com.android.art/javalib/core-oj.jar:/apex/com.android.art/javalib/core-libart.jar:/apex/com.android.art/javalib/core-icu4j.jar:/apex/com.android.art/javalib/okhttp.jar:/apex/com.android.art/javalib/bouncycastle.jar:/apex/com.android.art/javalib/apache-xml.jar:/system/framework/framework.jar:/system/framework/ext.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/framework-atb-backward-compatibility.jar:/apex/com.android.conscrypt/javalib/conscrypt.jar:/apex/com.android.media/javalib/updatable-media.jar:/apex/com.android.mediaprovider/javalib/framework-mediaprovider.jar:/apex/com.android.os.statsd/javalib/framework-statsd.jar:/apex/com.android.permission/javalib/framework-permission.jar:/apex/com.android.sdkext/javalib/framework-sdkextensions.jar:/apex/com.android.wifi/javalib/framework-wifi.jar:/apex/com.android.tethering/javalib/framework-tethering.jar
目标就是要确保Android系统可以加载和使用自定义的JAR文件,需要将其添加到BOOTCLASSPATH
环境变量中。
那么该怎么在原由基础上设置环境变量呢?
还是Linux 的基操 ,执行export后再读取发现已经添加进去了
export BOOTCLASSPATH=$BOOTCLASSPATH:/system/framework/ClientDemo.jar rk3568_r:/ # echo $BOOTCLASSPATH ...................省略 :/system/framework/ClientDemo.jar
那么问题来了,这种设置环境变量的方式是临时的 可是我要开机的时候就系统级别的加载和system/framework下面的jar一样 怎么搞?
我在系统源码中搜遍了 grep -rn "BOOTCLASSPATH"
只在system目录下发现了
core/rootdir/Android.mk:159: $(hide) sed -e 's?%BOOTCLASSPATH%?$(PRODUCT_BOOTCLASSPATH)?g' $< >$@ core/rootdir/Android.mk:160: $(hide) sed -i -e 's?%DEX2OATBOOTCLASSPATH%?$(PRODUCT_DEX2OAT_BOOTCLASSPATH)?g' $@ core/rootdir/init.environ.rc.in:13: export BOOTCLASSPATH %BOOTCLASSPATH% core/rootdir/init.environ.rc.in:14: export DEX2OATBOOTCLASSPATH %DEX2OATBOOTCLASSPATH%
其中system/core/rootdir/init.environ.rc.in文件,这不就是我需要找的内容嘛
# set up the global environment on early-init export ANDROID_BOOTLOGO 1 export ANDROID_ROOT /system export ANDROID_ASSETS /system/app export ANDROID_DATA /data export ANDROID_STORAGE /storage export ANDROID_ART_ROOT /apex/com.android.art export ANDROID_I18N_ROOT /apex/com.android.i18n export ANDROID_TZDATA_ROOT /apex/com.android.tzdata export EXTERNAL_STORAGE /sdcard export ASEC_MOUNTPOINT /mnt/asec export BOOTCLASSPATH %BOOTCLASSPATH% export DEX2OATBOOTCLASSPATH %DEX2OATBOOTCLASSPATH% export SYSTEMSERVERCLASSPATH %SYSTEMSERVERCLASSPATH% %EXPORT_GLOBAL_ASAN_OPTIONS% %EXPORT_GLOBAL_GCOV_OPTIONS% %EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS% %EXPORT_GLOBAL_HWASAN_OPTIONS%
我想尽快验证我的想法 懒得改代码重新编译 还好在之前的文章( Android系统 自定义动态修改init.custom.rc)中做了自定义可以直接pull/push 马上验证。
adb pull /vendor/etc/init/hw/init.custom.rc ----------- on post-fs-data export BOOTCLASSPATH=$BOOTCLASSPATH:/system/framework/ClientDemo.jar ----------- adb push init.custom.rc /vendor/etc/init/hw/init.custom.rc
重启发现还是没有设置成功 ? 这…
然后发现 在init.rc脚本中,export命令的行为与标准的Unix shell略有不同。
搜索发现系统源码中都是直接设置整个值,而不是追加新的值。
2.1. 将JAR文件放在适当的位置
首先确保自定义JAR文件(例如ClientDemo.jar
)已经被放置在/system/framework/
或其他适当的目录中。在mk文件中include $(BUILD_JAVA_LIBRARY)
是/system/framework
路径。
2.2. 修改启动脚本
BOOTCLASSPATH
通常在init.rc
或特定于设备的init.*.rc
文件中定义。为了添加自定义的JAR文件系统级加载,需要修改这些文件。
在init.custom.rc
或设备特定的init.*.rc
文件的post-fs-data
阶段,添加以下内容:
on post-fs-data mkdir /mnt/custom3 0755 root root export BOOTCLASSPATH /apex/com.android.art/javalib/core-oj.jar:/apex/com.android.art/javalib/core-libart.jar:/apex/com.android.art/javalib/core-icu4j.jar:/apex/com.android.art/javalib/okhttp.jar:/apex/com.android.art/javalib/bouncycastle.jar:/apex/com.android.art/javalib/apache-xml.jar:/system/framework/framework.jar:/system/framework/ext.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/framework-atb-backward-compatibility.jar:/apex/com.android.conscrypt/javalib/conscrypt.jar:/apex/com.android.media/javalib/updatable-media.jar:/apex/com.android.mediaprovider/javalib/framework-mediaprovider.jar:/apex/com.android.os.statsd/javalib/framework-statsd.jar:/apex/com.android.permission/javalib/framework-permission.jar:/apex/com.android.sdkext/javalib/framework-sdkextensions.jar:/apex/com.android.wifi/javalib/framework-wifi.jar:/apex/com.android.tethering/javalib/framework-tethering.jar:/system/framework/ClientDemo.jar:/system/framework/ServerDemo.jar
所以需要完整地列出了所有的BOOTCLASSPATH路径,包括新的和原始的,然后使用export命令一次性设置它。
2.3. 重新编译和刷入系统
如果不想每次手动 那必须改源代码了呀 需要对init.custom.rc
或设备特定的init
脚本进行了修改后,需要重新编译Android系统并刷入到设备上。
2.4. 验证
烧完设备重新启动,可以使用以下命令来检查BOOTCLASSPATH
是否已经更新:
echo $BOOTCLASSPATH
确保/system/framework/ClientDemo.jar
出现在输出中。
3.Android系统类加载流程
以下是学习的内容~
BOOTCLASSPATH
是Android系统中一个非常重要的环境变量,它定义了系统启动时Java类加载器应该加载哪些核心库。这些库包括Android框架的核心部分,以及其他关键的Java库。当Zygote进程启动时,它会使用BOOTCLASSPATH
来预加载这些类库,这样应用程序在启动时就可以快速地访问这些类。
BOOTCLASSPATH
在Android系统中的加载流程:
- 系统启动
当Android设备启动时,init
进程是第一个被启动的进程。init
进程负责初始化系统,启动各种服务,并设置各种环境变量,其中之一就是BOOTCLASSPATH
。可以查看system/core/rootdir/init.environ.rc.in
文件。
- 设置
BOOTCLASSPATH
在init.rc
或特定于设备的init.*.rc
文件中,BOOTCLASSPATH
被设置为一个包含多个JAR文件路径的字符串。这些JAR文件包含了Android系统运行所需的所有Java类。
- Zygote进程启动
init
进程在完成其初始化任务后会启动Zygote进程。Zygote是Android中所有应用进程的父进程,它预加载所有在BOOTCLASSPATH
中定义的类库,这样当新的应用进程启动时,它们可以快速地访问这些预加载的类。
- 类预加载
Zygote进程使用BOOTCLASSPATH
中定义的路径来预加载类。这意味着它会读取每个JAR文件,并加载其中的所有Java类。这个预加载过程确保了应用程序在启动时可以快速地访问这些类,而不需要从磁盘上重新加载它们。
- 应用进程启动
当你启动一个应用程序时,一个新的进程会从Zygote进程fork出来。由于Zygote已经预加载了所有的核心类,所以新的应用进程可以立即访问这些类,而不需要等待。 - 应用程序使用类库
应用程序在运行时会使用许多核心类库,这些类库已经被预加载到Zygote进程中,所以应用程序可以快速地访问它们。
BOOTCLASSPATH
在Android系统的启动过程中的作用:
它定义了系统应该加载哪些核心Java类库,确保应用程序在启动时可以快速地访问这些类。
希望这篇博客能帮助你理解如何在Android系统中加载自定义的JAR文件。如果有任何其他问题,请留言。