注册

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

前言



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

    • 阅读源码的能力
    • 阅读字节码的能力
    • 怀疑的态度



阅读源码的能力



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

为什么需要具有阅读源码的能力呢?

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


一、AppBarLayout阴影问题



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

image.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 解决隐私方法问题


  • 项目地址:github.com/Peakmain/As…
  • 大家可以去看下我的源码和文章,具体细节我就不阐述了,里面涉及到了大量的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代码块()是在初始化的过程中调用的
  • 虚拟机会必须保证一个类的方法在多线程下被同步加锁
  • 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权限封装框架,欢迎大家讨论。

作者:peakmain9
链接:https://juejin.cn/post/7173266221444366372
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册