调查Launcher无法启动原因
1.问题现象
车机启动之后,Launcher应用无法展示,导致车机桌面乃至依赖UI-Framework的应用都无法启动。
2.问题调查
取日志如下:
从日志中我们可以看出应用的BaseClassLoader无法从DexPathList
中加载到com.gxa.sdk.weather.WeatherChangeListener
类。
3.分析原因
3.1分析类加载流程
一看着ClassLoader就会联想到JAVA中类是如何加载的。一共分为七个步骤:
类加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
链接过程主要是将Java类的二进制代码合并到JVM的运行状态之中。
3.1.1 类加载
将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
3.1 .2 验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
3.1 .3 准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。
3.1 .4 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。
3.1 .5 初始化
初始化阶段是执行类构造器
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
- 虚拟机会保证一个类的
()方法在多线程环境中被正确加锁和同步。
3.2 类初始化顺序
1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法
从第二小节可以看出是初始化WeatherTimePresenter报的异常。然后细化分析,是初始化WeatherTimeModel报的异常。
我们反编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 | // classloader类 |
4.解决方案
我们知道了双亲委托机制
,对于该问题就有了解决办法:
- 父亲或者祖宗帮我把类找到
- 我自定义classloader去加载类
4.1 祖宗加载法
如果是自己做系统,有办法修改到祖宗的东西,我们就可以改为祖宗加载(bootstrap classLoader, 根加载器)。其实根加载器就是从**系统环境变量(BOOTCLASSPATH)**中定义的路径中去加载。
ui-framework属于系统包,最合理的方式还是系统加载。
因此,我们把自己需要加载的包(例如ui-framework)加入到环境变量区即可。
4.1.1 BOOTCLASSPATH被赋值流程分析
1 | 在system/core/rootdir/init.environ.rc.in中 |
通过上面的代码我们知道BOOTCLASSPATH环境变量等于%BOOTCLASSPATH%,那%BOOTCLASSPATH%是谁赋的值呢?难道是在Android.mk编译脚本中赋值的么,继续看
1 | system/core/rootdir/Android.mk |
从注解可以看出如果PRODUCT_BOOTCLASSPATH
变化了,init.environ.rc会重新生成。那么继续挖PRODUCT_BOOTCLASSPATH
在哪里改变的。
1 | 在build/make/core/dex_preopt.mk |
虽然看不大懂,但是从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 | 在build/make/target/product/core_minimal.mk中 |
总结:在该文件里面定义那些module.jar需要放入环境变量中。后面根加载器就从环境变量中加载类。配置好之后,重新编系统试试吧。
看到此处,使用根加载器配置就剧终了!!!!!!!!!!!!!!!!
4.2 自定义加载器
自定义加载器其基本原来就是替换app中的pathclassloader。将需要加载的dex包路径告诉自定义加载器去加载。
该方案也可以用于应用热修复。
该方案是20年了解热修复框架的时候写的demo代码。经过呕心吐血调试,终于将ui-framework没有加载到的WeatherChangeListener类加载到了。
测试结果如下:
该工程代码开源到我的github《JVMClassLoader》上了,明天下来集成试试。
注意:自定义加载器的方案应该是项目备选方案。
自定义加载器方案优点:稳定好使,但是需要应用集成HotFix.installPatchDex(this,new File(patchPath));
这么一句代码。
根加载器方案:ui-framework,car-framework作为公共组件库,还是应该采用4.1中描述的祖宗加载法,系统自动加载类,让应用零代码集成。