补:《Android面试题思考与解答》12月刊(三)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 日子过的好快,12月又过了,也就代表2020也要结束了。不管你在这一年中是开心,是难过,是苦闷,还是平淡,都过去了,向前看,老铁们~

类的生命周期


借用网上的一张图


8.png


类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。


类加载阶段


类的加载主要有三步:


  • 将class文件字节码内容加载到内存中。
  • 并将这些静态数据转换成方法区中的运行时数据结构。
  • 在堆中生成一个代表这个类的java.lang.Class对象。


我们编写的java文件会在编译后变成.class文件,类加载器就是负责加载class字节码文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定。


简单来说类加载机制就是从文件系统将一系列的 class 文件读入 JVM 内存中为后续程序运行提供资源的动作。


类加载器种类


类加载器种类主要有四种:


  • BootstrapClassLoader:启动类加载器,使用C++实现
  • ExtClassLoader:扩展类加载器,使用Java实现
  • AppClassLoader:应用程序类加载器,加载当前应用的classpath的所有类
  • UserDefinedClassLoader:用户自定义类加载器


属于依次继承关系,也就是上一级是下一级的父加载器(例外:BootstrapClassLoader无父子关系,属于根加载器)。


类加载过程(双亲委派机制)


类加载的过程可以用一句话概括:


先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区。


对于类加载器加载过程,就用到了双亲委派机制,具体如下:


当一个类加载器收到了类加载的请求,它不会直接去加载这类,而是先把这个请求委派给父加载器去完成,依次会传递到最上级也就是启动类加载器,然后父加载器会检查是否已经加载过该类,如果没加载过,就会去加载,加载失败才会交给子加载器去加载,一直到最底层,如果都没办法能正确加载,则会跑出ClassNotFoundException异常。


举例:


  • Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
  • Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
  • 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
  • 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
  • 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
  • 如果均加载失败,就会抛出ClassNotFoundException异常。

这么设计的原因主要有两点:


  • 这种层级关系可以避免类的重复加载。
  • 是为了防止危险代码的植入,比如String类,如果在AppClassLoader就直接被加载,就相当于会被篡改了,所以都要经过老大,也就是BootstrapClassLoader进行检查,已经加载过的类就不需要再去加载了。


Bitmap是什么,怎么存储图片。


Bitmap,位图,本质上是一张图片的内容在内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值,每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来。其中,A代表透明度,RGB代表红绿蓝三种颜色通道值。


Bitmap内存如何计算


Bitmap一直都是Android中的内存大户,计算大小的方式有三种:


  • getRowBytes() 这个在API Level 1添加的,返回的是bitmap一行所占的大小,需要乘以bitmap的高,才能得出btimap的大小
  • getByteCount() 这个是在API Level 12添加的,其实是对getRowBytes()乘以高的封装
  • getAllocationByteCount() 这个是在API Level 19添加的


这里我将一张图片放到项目的drawable-xxhdpi文件夹中,然后通过方法获取图片所占的内存大小:


var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test)
    img.setImageBitmap(bitmap)
    Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}")
    Log.e(TAG,"size = ${bitmap.allocationByteCount}")


打印出来的结果是


size=1960000


具体是怎么计算的呢?


图片内存=宽 * 高 * 每个像素所占字节


这个像素所占字节又和Bitmap.Config有关,Bitmap.Config是个枚举类,用于描述每个像素点的信息,比如:


  • ARGB_8888。常用类型,总共32位,4个字节,分别表示透明度和RGB通道。
  • RGB_565。16位,2个字节,只能描述RGB通道。


所以我们这里的图片内存计算就得出:


宽700 * 高700 * 每个像素4字节=1960000


Bitmap内存 和drawable目录的关系


首先放一张drawable目录对应的屏幕密度对照表,来自郭霖的博客:


7.jpg


刚才的案例,我们是把图片放到drawable-xxhdpi文件夹,而drawable-xxhdpi文件夹对应的dpi就是我们测试手机的dpi—480。所以图片的内存就是我们所计算的宽 * 高 * 每个像素所占字节


如果我们把图片放到其他的文件夹,比如drawable-hdpi文件夹(对应的dpi是240),会发生什么呢?


再次打印结果:


size = 7840000


这是因为一张图片的实际占用内存大小计算公式是:


占用内存 = 宽 * 缩放比例 * 高 * 缩放比例 * 每个像素所占字节


这个缩放比例就跟屏幕密度DPI有关了:


缩放比例 = 设备dpi/图片所在目录的dpi


所以我们这张图片的实际占用内存位:


宽700 * (480/240) * 高700 * (480/240) * 每个像素4字节 = 7840000


Bitmap加载优化?不改变图片质量的情况下怎么优化?


常用的优化方式是两种:


  • 修改Bitmap.Config


这一点刚才也说过,不同的Conifg代表每个像素不同的占用空间,所以如果我们把默认的ARGB_8888改成RGB_565,那么每个像素占用空间就会由4字节变成2字节了,那么图片所占内存就会减半了。


可能一定程度上会降低图片质量,但是我实际测试看不出什么变化。


  • 修改inSampleSize


inSampleSize,采样率,这个参数是用于图片尺寸压缩的,他会在宽高的维度上每隔inSampleSize个像素进行一次采集,从而达到缩放图片的效果。这种方法只会改变图片大小,不会影响图片质量。


val options=BitmapFactory.Options()
    options.inSampleSize=2
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test2,options)
    img.setImageBitmap(bitmap)


实际项目中,我们可以设置一个与目标图像大小相近的inSampleSize,来减少实际使用的内存:


fun getImage(): Bitmap {
        var options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.drawable.test2, options)
        // 计算最佳采样率
        options.inSampleSize = getImageSampleSize(options.outWidth, options.outHeight)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, R.drawable.test2, options)
    }


inJustDecodeBounds是什么?


上面的例子大家应该发现了,其中有个inJustDecodeBounds,又设置为true,又设置成false的,总感觉多此一举,那么他到底是干嘛呢?


因为我们要获取图片本身的大小,如果直接decodeResource加载一遍的话,那么就会增加内存了,所以官方提供了这样一个参数inJustDecodeBounds。如果inJustDecodeBounds为ture,那么decodebitmap为null,也就是不返回实际的bitmap,只把图片的大小信息放到了options的值中。


所以这个参数就是用来获取图片的大小信息的同时不占用内存。


Bitmap内存复用怎么实现?


如果有个需求,是在同一个imageview中可以加载不同的图片,那我们需要每次都去新建一个Bitmap对象,占用新的内存空间吗?如果我们这样写的话:


override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.actvitiy_bitmap)
        btn1.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test))
        }
        btn2.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test2))
        }
    }
    fun getBitmap(resId: Int): Bitmap {
        var options = BitmapFactory.Options()
        return BitmapFactory.decodeResource(resources, resId, options)
    }


这样就会Bitmap就会频繁去申请内存,释放内存,从而导致大量GC,内存抖动。


为了防止这种情况呢,我们就可以用到inBitmap参数,用于Bitmap的内存复用。这样同一块内存空间就可以被多个Bitmap对象复用,从而减少了频繁的GC。


val options by lazy {
        BitmapFactory.Options()
    }
    val reuseBitmap by lazy {
        options.inMutable = true
        BitmapFactory.decodeResource(resources, R.drawable.test, options)
    }
    fun getBitmap(resId: Int): Bitmap {
        options.inMutable = true
        options.inBitmap = reuseBitmap
        return BitmapFactory.decodeResource(resources, resId, options)
    }


这里有几个要注意的点


  • inBitmap要和inMutable属性配套使用,否则将无法复用。
  • Android 4.4之前,只能重用相同大小的 Bitmap 内存区域;4.4之后只要复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小即可。


所以一般在复用之前,还要判断下,新的Bitmap内存是不是小于可以复用的Bitmap内存,然后才能进行复用。


高清大图加载该怎么处理?


如果是高清大图,那就说明不允许进行图片压缩,比如微博长图,清明上河图。


所以我们就要对图片进行局部显示,这就用到BitmapRegionDecoder属性,主要用于显示图片的某一块矩形区域。


比如我要显示左上角的100 * 100区域:


fun setImagePart() {
        val inputStream: InputStream = assets.open("test.jpg")
        val bitmapRegionDecoder: BitmapRegionDecoder =
            BitmapRegionDecoder.newInstance(inputStream, false)
        val options = BitmapFactory.Options()
        val bitmap = bitmapRegionDecoder.decodeRegion(
            Rect(0, 0, 100, 100), options)
        image.setImageBitmap(bitmap)
    }


实际项目使用中,我们可以根据手势滑动,然后不断更新我们的Rect参数来实现具体的功能即可。


具体实现源码可以参考鸿洋的博客:https://blog.csdn.net/lmj623565791/article/details/49300989


如何跨进程传递大图?


  • Bundle直接传递。bundle最常用于Activity间传递,也属于跨进程的一种方式,但是传递的大小有限制,一般为1M。


//intent.put的putExtra方法实质也是通过bundle
intent.putExtra("image",bitmap);
bundle.putParcelable("image",bitmap)


Bitmap之所以可以直接传递,是因为其实现了Parcelable接口进行了序列化。而Parcelable的传递原理是利用了Binder机制,将Parcel序列化的数据写入到一个共享内存(缓冲区)中,读取的时候也会从这个缓冲区中去读取字节流,然后再反序列化成对象使用。这个共享内存也就是缓存区有一个大小限制—1M,而且是公用的。所以传图片的话很容易就容易超过这个大小然后报错TransactionTooLargeException


所以这个方案不可靠。


  • 文件传输


将图片保存到文件,然后只传输文件路径,这样肯定是可以的,但是不高效。


  • putBinder


这个就是考点了。通过传递binder的方式传递bitmap。


//传递binder
val bundle = Bundle()
bundle.putBinder("bitmap", BitmapBinder(mBitmap))
//接收binder中的bitmap
val imageBinder: BitmapBinder = bundle.getBinder("bitmap") as BitmapBinder
val bitmap: Bitmap? = imageBinder.getBitmap()
//Binder子类
class BitmapBinder :Binder(){
    private var bitmap: Bitmap? = null
    fun ImageBinder(bitmap: Bitmap?) {
        this.bitmap = bitmap
    }
    fun getBitmap(): Bitmap? {
        return bitmap
    }
}


为什么用putBinder就没有大小限制了呢?


  • 因为putBinder中传递的其实是一个文件描述符fd,文件本身被放到一个共享内存中,然后获取到这个fd之后,只需要从共享内存中取出Bitmap数据即可,这样传输就很高效了。
  • 而用Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制。


文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是1,依此类推。


描述new一个对象的过程


先上图,再描述:


4.png


5.png


6.png


Java中对象的创建过程包括 类初始化和类实例化两个阶段。而new就是创建对象的一种方式,一种时机。


当执行到new的字节码指令的时候,会先判断这个类是否已经初始化,如果没有初始化就要进行类的初始化,也就是执行类构造器<clinit>()方法。如果已经初始化了,就直接进行类对象的实例化。


  • 类的初始化,是类的生命周期中的一个阶段,会为类中各个类成员赋初始值。
  • 类的实例化,是指创建一个类的实例的过程。


但是在类的初始化之前,JVM会保证类的装载,链接(验证、准备、解析)四个阶段都已经完成,也就是上面的第一张图。


  • 装载是指 Java虚拟机查找.class文件并生成字节流,然后根据字节流创建java.lang.Class对象的过程。
  • 链接是指验证创建的类,并将其解析到JVM中使之能够被 JVM 执行。


那到底类加载的时机是什么时候呢?JVM 并没有规范何时具体执行,不同虚拟机的实现会有不同,常见有以下两种情况:


  • 隐式装载:在程序运行过程中,当碰到通过 new 等方式生成对象时,系统会隐式调用 ClassLoader 去装载对应的 class 到内存中;
  • 显示装载:在编写源代码时,主动调用 Class.forName() 等方法也会进行 class 装载操作,这种方式通常称为显示装载。


所以到这里,大的流程框架就搞清楚了:


  • JVM碰到new字节码的时候,会先判断类是否已经初始化,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行装载、验证、准备、解析四个阶段),然后进行类初始化
  • 如果已经初始化过了,就直接开始类对象的实例化工作,这时候会调用类对象的<init>方法。


结合例子说明


然后说说具体的逻辑,结合一段类代码:


public class Run {
    public static void main(String[] args) {
        new Student();
    }
}
public class Person{
    public static int value1 = 100;
    public static final int value2 = 200;
    public int value4 = 400;
    static{
        value1 = 101;
        System.out.println("1");
    }
    {
        value1 = 102;
        System.out.println("3");
    }
    public Person(){
        value1 = 103;
        System.out.println("4");
    }
}
public class Student extends Person{
    public static int value3 = 300;
    public int value5 = 500;
    static{
        value3 = 301;
        System.out.println("2");
    }
    {
        value3 = 302;
        System.out.println("5");
    }
    public Student(){
        value3 = 303;
        System.out.println("6");
    }
}


  • 首先是类装载,链接(验证、准备、解析)。


  • 当执行类准备过程中,会对类中的静态变量分配内存,并设置为初始值也就是“0值”。比如上述代码中的value1,value3,会为他们分配内存,并将其设置为0。但是注意,用final修饰静态常量value2,会在这一步就设置好初始值102。


  • 初始化阶段,会执行类构造器<clinit>方法,其主要工作就是初始化类中静态的(变量,代码块)。但是在当前类的<clinit>方法执行之前,会保证其父类的<clinit>方法已经执行完毕,所以一开始会执行最上面的父类Object的<clinit>方法,这个例子中会先初始化父类Person,再初始化子类Student。


  • 初始化中,静态变量和静态代码块顺序是由语句在源文件中出现的顺序所决定的,也就是谁写在前面就先执行谁。所以这里先执行父类中的value1=100,value1 = 101,然后执行子类中的value3 = 300,value3 = 301


  • 接着就是创建对象的过程,也就是类的实例化,当对象被类创建时,虚拟机会分配内存来存放对象自己的实例变量和父类继承过来的实例变量,同时会为这些事例变量赋予默认值(0值)。


  • 分配完内存后,会初始化父类的普通成员变量(value4 = 400),和执行父类的普通代码块(value1=102),顺序由代码顺序决定。


  • 执行父类的构造函数(value1 = 103)


  • 父类实例化完了,就实例化子类,初始化子类的普通成员变量(value5 = 500),执行子类的普通代码块(value3 = 302),顺序由代码顺序决定。


  • 执行子类的构造函数(value3 = 303)


所以上述例子打印的结果是:


123456


总结一下执行流程就是:


  1. 父类静态变量和静态代码块;
  2. 子类静态变量和静态代码块;
  3. 父类普通成员变量和普通代码块;
  4. 父类的构造函数;
  5. 子类普通成员变量和普通代码块;
  6. 子类的构造函数。


目录
相关文章
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
155 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
61 8
|
4月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
79 1
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
63 6
|
4月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
67 3
|
4月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
63 2
|
4月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
58 1
|
4月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
53 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
28 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
35 0
下一篇
无影云桌面