最近在做一个需求: 把项目里的Glide库全部迁移为Fresco
,然后就遇到一个比较蛋疼的事情,Fresco的View在初次使用前需要先初始化一下,调用FrescoHelper.initialize(),于是我就把这个初始化的调用放在了Application中主线程中了,然后就有同学反馈说这个比较耗时,打点统计了一下这个耗时大概在80ms左右,的确需要优化。
同步太慢,改成异步呗,需要调用的时候再阻塞检查即可
。
项目中的有两个模块之前就已经在使用Fresco,查看之前的初始化的方式是: 在首页打开时去异步初始化,然后再进入模块页面的时候再阻塞主线程去检查是否初始化完成,如果没有初始化完成再同步的初始化
。其中FrescoHelper.initialize()方法里会加锁判断是否已经初始化,已经初始化就直接跳过,未初始化的话会加锁进行初始化过程。
这样一来只要是有模块入口的地方都需要阻塞主线程调用一下这个检查是否初始化的操作,大致看了下现在代码里有十几处调用。
这样是不是不太优雅?新增一个入口就需要加一个检查,万一忘了加呢?就有可能发生未初始化完成Fresco调用DraweeView的crash,即这个异常: SimpleDraweeView was not initialized!
上面的思路需要调用的地方太多,需要梳理逻辑还可能忘了加,而且这次的迁移为Fresco会更复杂,所有Glide的调用都需要改为Fresco,这样一来入口非常的多,二来启动后可能马上就有Fresco的View需要立即加载。不能简单的同步主线程初始化,又想启动后马上使用,入口看上去又非常多无法预判,那还有什么思路呢?
于是很快就想到一个简单的办法,Fresco的View都需要采用SimpleDraweeView来加载,为了避免直接引用Fresco的api代码中都不会直接使用SimpleDraweeView,而是会继承它实现一个自己的一个ImageView这样来隔一层以便之后再有替换其他图片库,在我的项目中名字为XImageView,于是可以认为 XImageView是Fresco的入口,我们只需要在这个入口处添加初始化代码即可
,
public class XImageView extends SimpleDraweeView { public XImageView(Context context) { super(context); initView(); } public XImageView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { FrescoHelper.initialize(); ... } } 复制代码
类似上面的代码,在XImageView的构造函数里添加校验Fresco初始化的代码,这样写完一运行,发现还是会crash,报上面的异常 SimpleDraweeView was not initialized!
于是查看SimpleDraweeView里的源码发现是下面这样的:
public class SimpleDraweeView extends GenericDraweeView { public SimpleDraweeView(Context context, GenericDraweeHierarchy hierarchy) { super(context, hierarchy); this.init(context, (AttributeSet)null); } private void init(Context context, @Nullable AttributeSet attrs) { try { FrescoSystrace.beginSection("SimpleDraweeView#init"); if (this.isInEditMode()) { return; } Preconditions.checkNotNull(sDraweecontrollerbuildersupplier, "SimpleDraweeView was not initialized!"); ... } } } 复制代码
Preconditions.checkNotNull(sDraweecontrollerbuildersupplier, "SimpleDraweeView was not initialized!");
是Fresco对于是否初始化的检查,它是在SimpleDraweeView的构造函数里就执行了,也就是在我们封装的XImageView的构造函数的super里就执行了。
放在super的下面执行已经晚了,于是很自然的想法是把这个代码放在super前面不就行了吗?
public class XImageView extends SimpleDraweeView { public XImageView(Context context) { initView(); super(context); } private void initView() { FrescoHelper.initialize(); ... } } 复制代码
把初始化的代码放在super的上面,上面这样的代码直接提示编译错误了: Call to 'super()' must be first statement in constructor body
放到super之前为什么不行?不行的话还有什么思路?说到这里,先需要打断一下回顾Java里的几个基础知识了
首先,抛出一个问题:Android中自定义View大家都写过,例如我们打算自定义一个ImageView的时候,先继承于ImageView写出下面这样的代码:
//编译错误 There is no default constructor available in 'android.widget.ImageView' public class CustomTextView extends ImageView { } 复制代码
然后就会发现有一个编译的错误: There is no default constructor available in 'android.widget.ImageView'
解释上面的编译错误需要理解下面这几点
class A {} void test1() { A a = new A(); } 复制代码
class B { public B(String s){ } } void test2() { //编译错误 //B b = new B(); B b2 = new B("str"); } 复制代码
class C { public C() { System.out.println("C()"); } } class D extends C { public D() { System.out.println("D()"); } } @Test public void test3() { D d = new D(); } 复制代码
输出的日志如下,可以看出是先调用父亲的构造函数,再调用的子类:
C() D() 复制代码
class E { public E() { } public E(String s) { } } class F extends E { } public void test4() { F f = new F(); //编译错误 F f2 = new F("str"); } 复制代码
如上面代码所示F继承不到父类E中的有参构造函数,F里的无参构造也是默认生成的而不是继承过来的。
有了上面的理论基础,要解释上面的异常 There is no default constructor available in 'android.widget.ImageView'
就很容易了:
于是我们就需要显示的申明构造函数,告诉编译器我们需要调用父类的哪一个构造函数,于是super关键字就上场了...
Java里的super关键字存在于构造函数里,用于指明构造我的时候选用我父类的哪一个构造函数
class H { public H(String s1) { } public H(String s1, String s2) { } } class I extends H { public I() { super("s1"); } public I(String s2) { super("s1", s2); } } 复制代码
super关键字很简单,但是有一个要求,就是super需要放在构造函数里的第一行,如果不是放在第一行,就会报编译错误 Call to 'super()' must be first statement in constructor body
,
原因有了上面的基础知识也很好理解:
class I extends H { public I() { //编译错误 Call to 'super()' must be first statement in constructor body System.out.println("I"); super("s1"); } } 复制代码
类似的构造函数里的this(xx)调用自身构造函数也是需要放在最前面,跟super类似,不然也会有 Call to 'this()' must be first statement in constructor body
到这里,我们解释清楚了super前不能插入子类的一些代码逻辑,那Fresco的启动的调用是不是没有办法了呢?这时候就需要再回顾一下Java的基础知识:代码执行顺序
首先将一个Java类的代码分为如下的类别:静态方法、静态代码块、成员变量、代码块、构造函数、成员函数,他们在类初始化的时候是怎样一个顺序呢?
static class J { private String g = "g"; private static String f = "f"; { System.out.println("J {}1"); } { System.out.println("J {}2"); } static { System.out.println("J static {} 1"); } static { System.out.println("J static {} 2"); } private String d = "d"; private static String e = "e"; private String a; public J(String a) { System.out.println("J 1"); } public J(String a, String b) { System.out.println("J 2"); } } static class K extends J { private String g = "g"; private static String f = "f"; { System.out.println("K {}1"); } { System.out.println("K {}2"); } static { System.out.println("K static {} 1"); } static { System.out.println("K static {} 2"); } private String d = "d"; private static String e = "e"; private String a; public K(String a) { super(a); System.out.println("K 1"); } public K(String a, String b) { super(a, b); System.out.println("K 2"); } } @Test public void test5() { K k = new K("k"); } 复制代码
执行上面的代码,查看日志:
J static {} 1 J static {} 2 K static {} 1 K static {} 2 J {}1 J {}2 J 1 K {}1 K {}2 K 1 复制代码
另外想查看静态成员变量或者成员变量的赋值的顺序可以采用debug调试,一步一步step into就能得出下面这样的结论,代码执行的先后顺序总结如下:
注意静态方法和静态代码块之间没有先后顺序,是按照代码里书写的先后顺序执行 注意成员变量和代码块之间没有先后顺序,是按照代码里书写的先后顺序执行
注意上面的 静态方法和静态代码块是classloader加载阶段首次才会执行,之后再进行new操作是不会再触发,也就是静态的只会执行一次
。
@Test public void test6() { try { Class.forName("com.example.demoa.TestConstructor$K"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } 复制代码
同样,如果是通过Class.forName这样反射K的话,也是只会执行静态代码里的代码,日志如下:
J static {} 1 J static {} 2 K static {} 1 K static {} 2 复制代码
有了这个理论基础,上面Fresco的初始化校验的时机就有思路了,放在super之后时机太晚,代码上又不能放在super之前,而又需要放在父类SimpleDraweeView的构造函数之前,从上面打印的先后顺序中知道,将这个初始化逻辑放在子类XImageView的static静态代码块中即可,它的时机是类加载的时候,是在父类的构造函数执行之前,而且也只会执行一次,不会有放在构造函数中多次调用的多余的浪费的执行次数
public class XImageView extends SimpleDraweeView { static { FrescoHelper.initialize(); } } 复制代码
放在静态代码块之后即可以实现App启动时候异步初始化Fresco,然后在Fresco的View真正需要用到的时候才去阻塞主线程去校验是否初始化完成,这样真正的按需初始化,最大化的减少对主线程的影响而又几乎不影响fresco图片的加载,方案非常简单也是非常基础的技术,有时候简单的基础的也会是个完美的方案~