[Android][踩坑]Android Studio导入framework.jar的各种坑
前言
需求是这样的,我在基于system_server的源码开发一个功能,涉及了20多个类的增加。因此纯文本编辑器显然不是一个很好的选择。而配置像VS Code插件之类的事情又比较麻烦。因此导入到Android Studio中进行开发,并打包成jar包放回源码中参与编译显然是比较好的选择了。熟不知,这里面坑不少,本文主要就是记录一下自己的踩坑历程,同时给有需要的同仁一些参考,顺便吐槽下现在能在网上搜到的方案都千篇一律(同一段代码抄得到处都是 -_- |||)
提前声明,本文以自己的特殊使用场景触发,可能并不适用于所有同仁,且有些解决方法可能并不是最优解;欢迎留言讨论;
正文
坑一 在哪里找到可以用来导入的framework.jar ?
网上一堆人说(虽然根源都是那么几个,被无数转载了)在这里:
out/target/common/obj/JAVA_LIBRARY/framework_interminate/classes.jar
然而,看看最近几代的Android的目录结构就知道,编译的中间文件并不在这里面;
那么,针对较新的平台,可以在这里找到:
(Android N/O太久远了,未考证,按照理论来说,引入soong的Android N及其以后应该都是遵循下方的目录结构了)
Android 9/10:
out/soong/.intermediates/frameworks/base/framework/android_common/combined/framework.jar
Android 11:
out/soong/.intermediates/frameworks/base/framework-minus-apex/android_common/combined/framework-minus-apex.jar
坑二 如何导入?
Android Studio 3.x
以下以Android Studio 3.6.3为例(如果有更新版本的不兼容,欢迎留言,我验证好后更新)
将上方获取到的framework.jar拷贝到需要引用的模块(module,下同)中;
普遍网上的文章都是拷贝到app这个模块内,即:
<$project>/app/libs/framework.jar
同理,如果你跟我一样,是单独的一个模块在使用,比如我的模块名叫framework-ext,那么就放到这里:
<$project>/framework-ext/libs/framework.jar
然后修改对应模块的build.gradle
dependencies {
...
compileOnly files('libs/framework.jar')
}
此时如果你sync以下后发现很多之前标记为@hide的类已经可以找到了,比如android.os.SystemProperties;
但是不要高兴太早,当你引用一些标记为@hide的方法时,又歇菜了,比如:
android.os.UserHandle.myUserId();
到这里,可能你会搜到一些教你手动调整framework-ext.iml文件中orderEntry的教程,但是这里,一劳永逸的方法是在你在该模块的build.gradle中添加如下代码:
preBuild {
doLast {
//此处文件名根据实际情况修改:
def imlFile = file("framework-ext.iml")
try {
def parsedXml = (new XmlParser()).parse(imlFile)
def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
parsedXml.component[1].remove(jdkNode)
def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform"
new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
} catch (FileNotFoundException e) {
// nop, iml not found
println "no iml found"
}
}
}
然后,执行以下preBuild这个任务,目录结构在这里:
此时再去查看framework-ext.iml文件,发现Android SDK的优先级已经放到最后了,并且此时代码中对@hide方法的引用已经不标红了;
但是编译依旧报错;
这是因为编译时的bootclasspath并没有引入你的framework.jar;
因此还需要在项目的build.gradle中添加如下代码:
allprojects {
repositories {
google()
jcenter()
}
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs.add('-Xbootclasspath/p:libs/framework.jar')
}
}
}
注意,这里是整个project编译时都会走的逻辑,所以这里的classpath是每个模块的相对路径,不要写app/libs/framework.jar之类的。
另外,如果你跟我一样,还需要把services.core.priorityboosted.jar导进来,那么需要这么写:
options.compilerArgs.add('-Xbootclasspath/p:libs/framework.jar:libs/services.core.priorityboosted.jar')
用冒号:隔开即可;
切记不要写成两行!
切记不要写成两行!
切记不要写成两行!
至于为啥,我们需要了解-Xbootclasspath这个参数的意思:
-Xbootclasspath: 覆盖-bootclasspath的内容
-Xbootclasspath/a: 追加在-bootclasspath的内容之后
-Xbootclasspath/p: 插入到-bootclasspath的内容之前
我们这里用的/p,也就是让framework.jar与services.core.priorityboosted.jar插入到Android.jar之前,但是这里参数是不可重复的;
如果我们写成这样:
options.compilerArgs.add('-Xbootclasspath/p:libs/framework.jar')
options.compilerArgs.add('-Xbootclasspath/p:libs/services.core.priorityboosted.jar
')
那么编译参数会解析为:
15:06:05.818 [DEBUG] [org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler] Compiler arguments:
-source 1.8
-target 1.8
-d /***/dev_tools/android-projects/FrameworkImportDemo/framework-ext/build/intermediates/javac/release/classes
-encoding UTF-8
-bootclasspath /***/dev_tools/android-sdk/platforms/android-29/android.jar:/***/dev_tools/android-sdk/build-tools/29.0.2/core-lambda-stubs.jar
-g
-sourcepath
-proc:none -s /data/dev_tools/android-projects/FrameworkImportDemo/framework-ext/build/generated/ap_generated_sources/release/out
-XDuseUnsharedTable=true
-classpath ***
-Xbootclasspath/p:libs/framework.jar
-Xbootclasspath/p:libs/services.core.priorityboosted.jar
...
可以看到,这里会定义两次-Xbootclasspath/p,那么后声明的会覆盖掉前面的,那么-Xbootclasspath/p:libs/framework.jar 这一次传参实际上就是不生效了;
至此,执行以下assemble任务,jar包就可以正常编译出来了:
ll framework-ext/build/intermediates/aar_main_jar/*/*.jar
-rw-r--r-- 1 *** *** 595211 4月 28 16:38 framework-ext/build/intermediates/aar_main_jar/debug/classes.jar
-rw-r--r-- 1 *** *** 595128 4月 28 16:38 framework-ext/build/intermediates/aar_main_jar/release/classes.jar
Android Studio 4.x
以截止目前最新的Android Studio Preview (Arctick Fox 2020.3.1 Beta 3版本号203.7717.56.2031.7395685)为例:
由于没有了iml中间文件,且-Xbootclasspath/p修改方式不生效;因此Android Studio 3.x的修改方法不适用于Android Studio 4.x上;
经验证,修改bootclasspath的方法是,在需要修改的模块的build.gradle中添加如下编译参数修改:
android {
...
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
Set<File> fileSet = options.bootstrapClasspath.getFiles()
List<File> newFileList = new ArrayList<>();
//JAVA语法,可连续调用,输入参数建议为相对路径
newFileList.add(new File("libs/services.core.priorityboosted.jar"))
newFileList.add(new File("libs/framework.jar"))
//最后将原始参数添加
newFileList.addAll(fileSet)
options.bootstrapClasspath = files(
newFileList.toArray()
)
}
}
}
即可;
Android Studio Electric Eel (2022.1.1 Patch 2)
2023-4-12更新:
观察到当前版本使用javac编译时,-bootclasspath
参数已经不存在,因此修改-classpath
参数即可实现类似效果:
android {
...
gradle.projectsEvaluated {
tasks.getByName("compileDebugJavaWithJavac") {
//经过验证发现,修改classpath会导致编译task列表顺序紊乱,从而报错提示找不到R.class相关内容;
//因此此处需要显式声明javac的task依赖resource编译完成
dependsOn("processDebugResources")
classpath = reorderClasspath(classpath.getFiles())
}
tasks.getByName("compileReleaseJavaWithJavac") {
//同上,release与debug分别声明对应的task依赖
dependsOn("processReleaseResources")
classpath = reorderClasspath(classpath.getFiles())
}
}
}
// 新增一个函数,以便多次调用
def reorderClasspath(Set<File> classpathSet) {
List<File> newFileList = new ArrayList<>()
File sdkFile = null
for (File f : classpathSet) {
//将android.jar放到-classpath参数末尾即可
if ("android.jar" == f.getName()) {
sdkFile = f
} else {
newFileList.add(f);
}
}
if (sdkFile != null) {
newFileList.add(sdkFile)
}
return files(
newFileList.toArray()
)
}
Android Studio Giraffe
感谢下方评论区李艺为提供的方法,这里直接贴原链接了:Android Studio Giraffe引用framework.jar方法
坑三 无法部署为Apk
直接部署依旧会报错,如果没报错,clean以后再部署也会报错;
看报错还是在compileDebugJavaWithJavac
这是因为部署apk的任务列表与单独编译compileDebugJavaWithJavac的内容不太一样;
具体原因由于这不是我的需求,就懒得分析了,这里给一个可以解决的办法,那就是在打包apk之前手动编译一次framework-ext:
有更好的解决方法可以在评论区讨论;