[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:
在这里插入图片描述
有更好的解决方法可以在评论区讨论;

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐