自己的练习项目Brush,是一个功能简单的绘制书写应用,在最后一个提交add simulate after launch之前,都存在一个问题,就是每次启动应用后,绘制第一条曲线的开始部分都是直线。原因是Android会根据应用的处理能力(UI线程的处理速度),控制
MotionEvent
的上报频率,也就是onTouchEvent
的触发频率。如果在UI线程中处理了耗时任务,那么onTouchEvent
的触发频率就会非常低,在这种情况下,只通过MotionEvent
的getX
和getY
方法来获取坐标值,根据数量极少的坐标点绘制贝塞尔曲线,有时就会出现近似直线的情况。Brush绘制第一条曲线的开始部分都是直线的问题,可以肯定的是UI线程中做了耗时操作,而且只有第一次中才存在。经过各种尝试,最后确定了是Java类加载耗时造成的。
Java类加载
对于Java类加载,先看看别人是怎么理解的:
Java类加载机制 Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
在Android中app运行时并不会把所有的类一次性全部加载到虚拟机中,而是需要用到时才去加载,来看看加载一个类加载耗时的问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/* MainActivity.java */
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
long time = System.nanoTime();
ClassLoadTest.callOnTime(time);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
time = System.nanoTime();
ClassLoadTest.callOnTime(time);
}
}
1 | /* ClassLoadTest */ |
非常小的一个类,主要检测开始调用方法到进入方法内部开始执行所花费的时间。这个应用运行在设备LGE Nexus 5X (Android 8.0.0, API 26)
上,看一下日志输出:1
2
309-01 16:33:41.656 15291-15291/me.huntto.classloadtest I/ClassLoadTest: init
callOnTime use:1206198ns
09-01 16:33:41.658 15291-15291/me.huntto.classloadtest I/ClassLoadTest: callOnTime use:7344ns
可以看到两次的时间不是一个数量级的,第一次是毫秒级,第二次是微秒级。如果callOnTime
方法里面使用了其他类,并且也是第一次加载,那么整个调用链第一次花费的时间将更长。
解决问题
类加载本来就是一个耗时的操作,如果要解决它,需要留给优化虚拟机的专家们。对于普通应用开发者来说,最好的解决方法就是规避问题。
如文章开头提到Brush就是对类加载耗时问题比较敏感。ClassLoadTest
中要让每次callOnTime
打印的时间不至于相差太大,可以让ClassLoadTest
提前加载:1
2
3
4
5
6
7
8
9/* MainActivity.java */
public class MainActivity extends AppCompatActivity {
static {
ClassLoadTest.load();
}
...
}
1 | /* ClassLoadTest */ |
1 | 09-01 16:48:32.929 15899-15899/me.huntto.classloadtest I/ClassLoadTest: init |
可见callOnTime
的时间差已经是微秒级的了。
后记
由于Java类加载比较耗时,所以要避免一次加载过多类,造成程序卡顿的现象,应该适时的加载适量的类。