转载

Android启动优化中的一个小tip(Java中基础知识的十万个为什么)

最近在做一个需求: 把项目里的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里的几个基础知识了

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'

为什么会有这样的编译错误呢?

解释上面的编译错误需要理解下面这几点

  • 1,类中不写构造函数时默认会有一个无参构造函数
class A {}

    void test1() {
        A a = new A();
    }
复制代码
  • 2,类中一旦书写了构造函数,默认的无参构造函数就消失了
class B {
        public B(String s){ }
    }

    void test2() {
        //编译错误
        //B b = new B();

        B b2 = new B("str");
    }
复制代码
  • 3,实例化子类的时候,需要先实例化父类。这个道理也比较好理解:不先有父亲哪来的儿子呢?
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()
复制代码
  • 4,构造函数不能被继承,也就是父类的构造函数只属于自己
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' 就很容易了:

  • 1,ImageView中有申明四个构造函数,且都是有参数的构造函数,申明了构造函数后默认的无参构造函数就没有了
  • 2,CustomImageView继承于ImageView,由于构造函数不能被继承,故编译器只会给CustomImageView生成默认的无参构造函数
  • 3,构造实例化子类的时候,需要先构造实例化父类,只有默认的无参构造函数的CustomImageView需要在它的默认构造函数里调用实例化父类ImageView,而ImageView没有无参数的构造函数就没法知道怎么构造了

于是我们就需要显示的申明构造函数,告诉编译器我们需要调用父类的哪一个构造函数,于是super关键字就上场了...

Java里的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 , 原因有了上面的基础知识也很好理解:

  • 1,实例化子类的时候,需要先实例化父类
  • 2,于是在调用子类构造函数的时候,需要先去调用父类的构造函数,所以super需要放在最前面去调用
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代码执行顺序:

首先将一个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图片的加载,方案非常简单也是非常基础的技术,有时候简单的基础的也会是个完美的方案~

原文  https://juejin.im/post/5e05f05b6fb9a016271291ba
正文到此结束
Loading...