BasicLibrary架构设计旅程(一)—Android必备技能

前言

  • 2022年对大部分人来说真的是不容易的一年,有不少粉丝私信问我,今年行情不好,但是现在公司又不好怎么办,我的建议就是学习。无论过去,现在,未来,投资自己一定是不会错的,只有当你足够强大,哪怕生活一地鸡毛,你也能垫起脚尖独揽星空。
  • 对于Android来说,我觉得有两个能力和一个态度一定要掌握
    1.阅读源码的能力
    2.阅读字节码的能力
    3.怀疑的态度

阅读源码的能力

  • 个人技巧:我个人阅读源码喜欢自己给自己提问题,随后带着问题去读源码的流程,当遇到不确定的可以看看别的大神写的博客和视频。
为什么需要具有阅读源码的能力呢?

当我们通过百度搜索视频,博客,stackOverflow找不到我们问题解决办法的时候,可以通过阅读源码来寻找问题,并解决问题,如以下两个案例

一、AppBarLayout阴影问题

  • 源码地址:https://github.com/Peakmain/BasicUI/wiki/NavigationBar
  • 我们每次在项目添加头部的时候,一般做法都是说定义一个公用的布局,但是这其实并不友好,而且每次都需要findVIewById,为了解决上述问题,我用了Builder设计模式设计了NavigationBar,可以动态添加头部
  • 其中有个默认的头部设计DefaultNavigationBar,使用的是AppBarLayout+ToolBar,AppBarLayout有个问题就是会存在阴影,我想要在不改变布局的情况下,动态设置取消阴影,在百度中得到的前篇一律的答案是,设置主题,布局中设置阴影


    百度的结果.png
  • 既然说布局中设置elevation有效,那么是否可以通过findViewById找到AppBarLayout然后设置elevation=0
findViewById<AppBarLayout>(R.id.navigation_header_container).elevation=0f

运行之后,发现阴影还仍然存在

  • 既然布局中设置elevation有效,那它的源码怎么写的呢?
    我们可以在AppBarLayout的构造函数中找到这行代码


    image.png

    我们可以发现最终调用的是一个非公平类的静态方法,直接将方法拷贝到我们自己的项目,之后调用该方法

  static void setDefaultAppBarLayoutStateListAnimator(
      @NonNull final View view, final float elevation) {
    final int dur = view.getResources().getInteger(R.integer.app_bar_elevation_anim_duration);

    final StateListAnimator sla = new StateListAnimator();

    // Enabled and liftable, but not lifted means not elevated
    sla.addState(
        new int[] {android.R.attr.state_enabled, R.attr.state_liftable, -R.attr.state_lifted},
        ObjectAnimator.ofFloat(view, "elevation", 0f).setDuration(dur));

    // Default enabled state
    sla.addState(
        new int[] {android.R.attr.state_enabled},
        ObjectAnimator.ofFloat(view, "elevation", elevation).setDuration(dur));

    // Disabled state
    sla.addState(new int[0], ObjectAnimator.ofFloat(view, "elevation", 0).setDuration(0));

    view.setStateListAnimator(sla);
  }
image.png

二、Glide加载图片读取设备型号问题

  • 再比如App加载网络图片时候,App移动应用检测的时候说我们应用自身获取个人信息行为,描述说的是我们有图片上传行为,看了堆栈,主要问题是加载图片的时候,user-Agent有读取设备型号行为


    image.png
  • 关于这篇文章的源码分析,大家可以看我之前的文章:隐私政策整改之Glide框架封装
  • glide加载图片默认用的是HttpUrlConnection
  • 加载网络图片的时候,默认是在GlideUrl中设置了Headers.DEFAULT,它的内部会在static中添加默认的User-Agent。

小总结

  • 优秀的阅读源码能力可以帮我们快速定位并解决问题。
  • 优秀的阅读源码能力也可以让我们快速上手任何一个热门框架并了解其原理

阅读字节码的能力的重要性

当我们熟练掌握字节码能力,我们能够深入了解JVM,通过ASM实现一套埋点+拦截第三方频繁调用隐私方法的问题

字节码基础知识
  • 由于跨平台性的设计,java的指令都是根据栈来设计的,而这个栈指的就是虚拟机栈
  • JVM运行时数据区分为本地方法栈、程序计数器、堆、方法区和虚拟机栈

局部变量表

  • 每个线程都会创建一个虚拟机栈,其内部保存一个个栈帧,对应一次次方法的调用
  • 栈帧的内部结构是分为:局部变量表、操作数栈、动态链接(指向运行时常量池的方法引用)和返回地址
  • 局部变量表内部定义了一个数字数组,主要存储方法参数和定义在方法体内的局部变量
  • 局部变量表存储的基本单位是slot(槽),long和double存储的是2个槽,其他都是1个槽
  • 非静态方法,默认0槽位存的是this(指的是该方法的类对象)

操作数栈

  • 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈
  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  • 方法调用的开始,默认的操作数栈是空的,但是操作数栈的数组已经创建,并且大小已知
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问

一些常用的助记符

  • 从局部变量表到操作数栈:iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload
  • 操作数栈放到局部变量表:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstor_,astore,astore_
  • 把常数放到到操作数栈:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
  • 取出栈顶两个数进行相加,并将结果压入操作数栈:iadd,ladd,fadd,dadd
  • iinc:对局部变量表的值进行加1操作
i++和++i区别
public class Test {

    public static void main(String[] args) {
       int i=10;
       int a=i++;
       int j=10;
       int b=++j;
        System.out.println(i);
        System.out.println(a);
        System.out.println(j);
        System.out.println(b);
    }
}
  • 大家可以思考下,这个结果会是什么呢?
  • 结果分别是11 10 11 11

字节码结果分析

  • 查看字节码命令:javap -v Test.class
  • 大家也可以使用idea自带的jclasslib工具,或者ASM Bytecode Viewer工具
 0 bipush 10
 2 istore_1
 3 iload_1
 4 iinc 1 by 1
 7 istore_2
 8 bipush 10
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4
17 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
20 iload_1
21 invokevirtual #3 <java/io/PrintStream.println : (I)V>
24 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
27 iload_2
28 invokevirtual #3 <java/io/PrintStream.println : (I)V>
31 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
34 iload_3
35 invokevirtual #3 <java/io/PrintStream.println : (I)V>
38 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
41 iload 4
43 invokevirtual #3 <java/io/PrintStream.println : (I)V>
46 return
  • 由于我们是非静态方法,所以局部变量表0的位置存储的是this


    image.png
  • bipush 10:将常量10压入操作数栈


    image.png
  • istore1:将操作数栈的栈顶元素放入到局部变量表1的位置


    image.png
  • iload1:将局部变量表1的位置放入到操作数栈


    image.png
  • iinc 1 by 1:局部变量表1的位置的值+1


    image.png
  • istore2:将操作栈的栈顶元素压入局部变量表2的位置


    image.png
  • 至此最上面两行代码执行完毕,下面的代码我就不再画图阐述了,我相信机智聪敏的你一定已经学会分析了
  • 最后来一个小小的总结吧
    • i++是先iload1,后局部变量表自增,再istore2,所以a的值还是10
    • ++i是先局部变量表自增,随后iload,再istore,所以b的值已经变成了11
ASM 解决隐私方法问题
  • 项目地址:https://github.com/Peakmain/AsmActualCombat
  • 大家可以去看下我的源码和文章,具体细节我就不阐述了,里面涉及到了大量的opcodec的操作符,比如Opcode.ILOAD


    image.png

怀疑的态度

  • 无论是视频还是博客,大家对不确认的知识保持一颗怀疑的态度,因为一篇文章或者视频都有可能是不对的,包括我现在写的这篇文章。
kotlin object实现的单例类是懒汉式还是饿汉式
image.png

image.png
  • 以上两个都是网上的文章截取的文章,那kotlin实现的object单例到底是饿汉式还是懒汉式的呢?
  • 假设我们有以下代码
object Test {
    const val TAG="test"
}

通过工具看下反编译后的代码


image.png

image.png

static代码块什么时候初始化呢?

  • 首先我们需要知道JVM的类加载过程:loading->link->初始化
  • link又分为:验证、准备、解析
  • 而static代码块(<cinit>)是在初始化的过程中调用的
  • 虚拟机会必须保证一个类的<cinit>方法在多线程下被同步加锁
  • Java使用方式分为两种:主动和被动


    image.png
  • 主动使用才会导致static代码块的调用

单例的懒汉式和饿汉式的区别是什么呢

  • 懒汉式:类加载不会导致该实例被创建,而是首次使用该对象才会被创建
  • 饿汉式:类加载就会导致该实例对象被创建


    image.png
public class Test {
    private static Test mInstance;
    static {
        System.out.println("static:"+mInstance);
    }
    private Test() {
        System.out.println("init:"+mInstance);
    }
    public static Test getInstance() {
        if (mInstance == null) {
            mInstance = new Test();
        }
        return mInstance;
    }
    public static void main(String[] args) {
        Test.getInstance();
    }
}
  • 当调用getInstance的时候,类加载过程中会进行初始化,也就是调用static代码块
  • static代码块执行时,由于类没有实例化,所以获取到是null。
  • 也就是说,类加载的时候并没有对该实例进行创建(懒汉式)
public class Test1 {
    private static final Test1 mInstance=new Test1();

    private Test1(){
        System.out.println("init:"+mInstance);
    }
    static {
        System.out.println("static:"+mInstance);
    }
    public static Test1 getInstance(){
        return mInstance;
    }

    public static void main(String[] args) {
        Test1.getInstance();
    }
}
  • 类的初始化顺序是由代码的顺序来决定的,上面的代码首先对mInstance进行初始化,但是由于此时构造函数执行完成后才完成类的初始化,所以构造函数返回的是null
  • static代码块执行的时候,类实例已经创建完毕
  • 正如上面说的static代码块执行的时候还处于类加载中的初始化状态,所以实例是在初始化之前完成(饿汉式)

我们现在回到kotlin的object,我们将其转成Java类

public class Test2 {
    public static final String TAG = "test";
    private Test2() {
        System.out.println("init:" + mInstance);
    }
    public static Test2 mInstance;
    static {
        Test2 test2 = new Test2();
        mInstance = test2;
        System.out.println("static:" + mInstance);
    }

    public static void main(String[] args) {
        System.out.println(Test2.TAG);
    }
}
  • 上面代码在static代码块的时候(类加载的初始化时)进行了类的实例初始化(饿汉式)

总结

  • Android必备的技能,其实很多,比如JVM、高并发、binder、泛型、AMS,WMS等等
  • 我个人觉得源阅读码能力和掌握字节码属于必备技能,能提高自己知识领域
  • 当然如我上面所说,要保持怀疑的态度,本文说的可能也不对。
  • 下一篇文章,我将介绍BasicLibrary中基于责任链设计模式搭建的Activity Results API权限封装框架,欢迎大家讨论。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容