1.问题现象

车机启动之后,Launcher应用无法展示,导致车机桌面乃至依赖UI-Framework的应用都无法启动。

2.问题调查

取日志如下:

apk中找不到需要的类.png

从日志中我们可以看出应用的BaseClassLoader无法从DexPathList中加载到com.gxa.sdk.weather.WeatherChangeListener类。

3.分析原因

3.1分析类加载流程

一看着ClassLoader就会联想到JAVA中类是如何加载的。一共分为七个步骤:

类加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

链接过程主要是将Java类的二进制代码合并到JVM的运行状态之中。

类加载过程.png

3.1.1 类加载

​ 将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

3.1 .2 验证

​ 确保加载的类信息符合JVM规范,没有安全方面的问题。

3.1 .3 准备

​ 正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。

3.1 .4 解析

​ 虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。

3.1 .5 初始化

​ 初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和**静态语句块(static块)**中的语句合并产生的。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
3.2 类初始化顺序

1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法

WeatherTimePresenter构造函数.png

从第二小节可以看出是初始化WeatherTimePresenter报的异常。然后细化分析,是初始化WeatherTimeModel报的异常。

WetherTimeModel.png

我们反编Launcher应用可以看到,WeatherChangeListener属于WeatherTimeModel的成员变量,会在类调用构造方法之前初始化。并且看左边框图目录树,并没有看到com.gxa.sdk.weather.WeatherChangeListener这个类。因此结合类加载双亲委托机制,可以判断到WeatherChangeListener即没有从根(Bootstrap)类加载中加载到,也没有从应用类加载器中加载到。

3.3 双亲委托机制

ClassLoader的双亲委托模式:classloader 按级别分为三个级别:最上级 : bootstrap classLoader(根类加载器) ; 中间级:extension classLoader (扩展类加载器) 最低级 app classLoader(应用类加载器)。

  • 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。

  • 扩展(Extension)类加载器:它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。

  • 系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// classloader类
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
// 检查类是不是已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 有父亲就一直往上找
if (parent != null) {
// 递归找父亲加载
c = parent.loadClass(name, false);
} else {
// 根就是祖宗了,没有父亲了。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

//如果父亲和祖宗都没有加载到
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 调用自己的findclass加载.
c = findClass(name);
}
}
return c;
}

4.解决方案

我们知道了双亲委托机制,对于该问题就有了解决办法:

  • 父亲或者祖宗帮我把类找到
  • 我自定义classloader去加载类
4.1 祖宗加载法

如果是自己做系统,有办法修改到祖宗的东西,我们就可以改为祖宗加载(bootstrap classLoader, 根加载器)。其实根加载器就是从**系统环境变量(BOOTCLASSPATH)**中定义的路径中去加载。

ui-framework属于系统包,最合理的方式还是系统加载。

因此,我们把自己需要加载的包(例如ui-framework)加入到环境变量区即可。

4.1.1 BOOTCLASSPATH被赋值流程分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在system/core/rootdir/init.environ.rc.in中
# set up the global environment
on init
export ANDROID_BOOTLOGO 1
export ANDROID_ROOT /system
export ANDROID_ASSETS /system/app
export ANDROID_DATA /data
export ANDROID_STORAGE /storage
export EXTERNAL_STORAGE /sdcard
export ASEC_MOUNTPOINT /mnt/asec
export BOOTCLASSPATH %BOOTCLASSPATH%
export SYSTEMSERVERCLASSPATH %SYSTEMSERVERCLASSPATH%
%EXPORT_GLOBAL_ASAN_OPTIONS%
%EXPORT_GLOBAL_GCOV_OPTIONS%

通过上面的代码我们知道BOOTCLASSPATH环境变量等于%BOOTCLASSPATH%,那%BOOTCLASSPATH%是谁赋的值呢?难道是在Android.mk编译脚本中赋值的么,继续看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# system/core/rootdir/Android.mk
# Regenerate init.environ.rc if PRODUCT_BOOTCLASSPATH has changed.
bcp_md5 := $(word 1, $(shell echo $(PRODUCT_BOOTCLASSPATH) $(PRODUCT_SYSTEM_SERVER_CLASSPATH) | $(MD5SUM)))
bcp_dep := $(intermediates)/$(bcp_md5).bcp.dep
$(bcp_dep) :
$(hide) mkdir -p $(dir $@) && rm -rf $(dir $@)*.bcp.dep && touch $@

$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/init.environ.rc.in $(bcp_dep)
@echo "Generate: $< -> $@"
@mkdir -p $(dir $@)
$(hide) sed -e 's?%BOOTCLASSPATH%?$(PRODUCT_BOOTCLASSPATH)?g' $< >$@
$(hide) sed -i -e 's?%SYSTEMSERVERCLASSPATH%?$(PRODUCT_SYSTEM_SERVER_CLASSPATH)?g' $@
$(hide) sed -i -e 's?%EXPORT_GLOBAL_ASAN_OPTIONS%?$(EXPORT_GLOBAL_ASAN_OPTIONS)?g' $@
$(hide) sed -i -e 's?%EXPORT_GLOBAL_GCOV_OPTIONS%?$(EXPORT_GLOBAL_GCOV_OPTIONS)?g' $@

从注解可以看出如果PRODUCT_BOOTCLASSPATH变化了,init.environ.rc会重新生成。那么继续挖PRODUCT_BOOTCLASSPATH在哪里改变的。

1
2
3
4
5
# 在build/make/core/dex_preopt.mk
# list of boot classpath jars for dexpreopt
DEXPREOPT_BOOT_JARS := $(subst $(space),:,$(PRODUCT_BOOT_JARS))
DEXPREOPT_BOOT_JARS_MODULES := $(PRODUCT_BOOT_JARS)
PRODUCT_BOOTCLASSPATH := $(subst $(space),:,$(foreach m,$(DEXPREOPT_BOOT_JARS_MODULES),/system/framework/$(m).jar))

虽然看不大懂,但是从PRODUCT_BOOTCLASSPATH这一行我们大概都可以猜到逻辑,foreach遍历/system/framework/下的所有jar包,并且subst将遍历到的包名之间空格替换成**’ : ‘**。

PRODUCT_BOOTCLASSPATH可以看到我们在系统中看到的环境遍历表示方式就类似于:**/system/framework/xx.jar:/system/framework/yy.jar:/system/framework/zz.jar**

然后看DEXPREOPT_BOOT_JARS猜这个标签就是收集所有的启动jar文件。继续顺藤grep。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 在build/make/target/product/core_minimal.mk中
# The order of PRODUCT_BOOT_JARS matters.
PRODUCT_BOOT_JARS := \
core-oj \
core-libart \
conscrypt \
okhttp \
legacy-test \
bouncycastle \
ext \
framework \
telephony-common \
voip-common \
ims-common \
apache-xml \
org.apache.http.legacy.boot \
android.hidl.base-V1.0-java \
android.hidl.manager-V1.0-java

总结:在该文件里面定义那些module.jar需要放入环境变量中。后面根加载器就从环境变量中加载类。配置好之后,重新编系统试试吧。

看到此处,使用根加载器配置就剧终了!!!!!!!!!!!!!!!!

4.2 自定义加载器

自定义加载器其基本原来就是替换app中的pathclassloader。将需要加载的dex包路径告诉自定义加载器去加载。

该方案也可以用于应用热修复。

该方案是20年了解热修复框架的时候写的demo代码。经过呕心吐血调试,终于将ui-framework没有加载到的WeatherChangeListener类加载到了。

测试结果如下:

dex中获取.png

从jar中获取.png

该工程代码开源到我的github《JVMClassLoader》上了,明天下来集成试试。

注意:自定义加载器的方案应该是项目备选方案。

自定义加载器方案优点:稳定好使,但是需要应用集成HotFix.installPatchDex(this,new File(patchPath));这么一句代码。
根加载器方案:ui-framework,car-framework作为公共组件库,还是应该采用4.1中描述的祖宗加载法,系统自动加载类,让应用零代码集成。

自定义加载器代码:https://github.com/oujie123/JVMClassLoader