Android面试记录

handler内存泄漏的原因:
匿名内部类默认持有外部类对象。
ThreadLocal -> Looper->MessageQueue -> Message -> handler->activity
ThreadLocal是 static 类型,不会被回收。
解决:
1、把handler变成static类型。静态匿名内部类不会持有外部引用。
2、removeMessages

MessageQueue存储的Message有上限吗?
没有。WMS:60hz屏幕:一次UI刷新3个msg。AMS:activity、fragment生命周期管理通过msg。

----------------------------------------------------------------------------------------


handler如何处理发送延迟消息?
消息的阻塞机制:Message是一个基于时间排序的单向列表。enqueueMessage()方法会通过for循环堆消息排序。
延迟处理:Message.next()->nativePollOnce()方法休眠MessageQueue。

jvm内存模型:
方法区:常量、静态变量、类信息、运行时常量池,操作的是直接内存
堆内存:堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
本地方法栈:运行本地方法的空间,也就是native本地方法运行时的一块空间。
虚拟机栈:栈帧中包含:局部变量表、操作数栈、动态链接和方法出口。
程序计数器: 程序计数器是用于存放下一条指令所在单元的地址的地方。

堆空间分代划分
堆被划分为新生代和老年代(Tenured),新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
GC- Garbage Collection 垃圾回收,在 JVM 中是自动化的垃圾回收机制。
GC root原理:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。


使用Message时应该如何创建它?
obtain() :复用,防止内存抖动。Looper中有一个sPool,存储处理完的消息列表。避免new Message的过程。

内存抖动:OOM、卡顿:单线程进行垃圾回收时会暂停所有工作线程,知道它回收结束(stop the world)。

----------------------------------------------------------------------------------------
Android性能优化:
1、绘制优化 防止过度嵌套,过度绘制。
开发者选项->Profile GPU Rendering 查看绘制耗时。
开发者选项->显示fps
开发者选项->过度绘制监测

布局层级检查
Tools > Android > Layout Inspector 

2、内存优化
内存泄漏:WebView造成的泄漏、资源对象未关闭、集合中的对象没清理、Bitmap对象、监听器未关闭、Handler内存泄漏
内存泄漏监控:LeakCanary第三方库。使用MAT分析内存泄漏,LeakCanary生成的hprof文件需要转换才能被MAT工具打开,这里可以使用AS工具转换。

3、内存空间优化
弱引用(WeakReference)
在Java中尽量使用基本数据类型。
使用最优的数据类型(HashMap与ArrayMap)。
不要大量使用枚举类型

4、稳定性优化

为什么会产生ANR
Activity的UI在5秒内没有响应输入事件(例如,按键按下,屏幕触摸)–主要类型。
BroadcastReceiver在10秒内没有执行完毕。
Service在特定时间内(20秒内)无法处理完成–小概率类型。

主线程频繁进行IO操作,比如读写文件或者数据库。
多线程操作的死锁,导致主线程等待超时。
主线程操作调用join()方法、sleep()方法或者wait()方法。

代码优化
避免创建Thread,推荐做法:利用线程池,利用第三方库如RxJava。

5、安装包大小优化
只使用一套图片,使用高分辨率的图片
图片使用WebP
移除无用的依赖库,保持代码的整洁。
Lint工具检查无用文件将无用的资源列在“UnusedResources: Unused resources”,删除。

--------------------------------------------------------------------------------------

屏幕适配
dp,它是 Android 特有的单位,它与设备上的实际物理像素点无关,只与屏幕像素密度有关,它可以保证在不同屏幕像素密度的设备上显示相同的效果。
dp 的优势也主要体现在相同尺寸,不同分辨率的设备的适配效果。
dp = 分辨率 / dpi *160

sw 限定符适配方案

使用wrap_content、match_parent、weight

使用限定符:尺寸限定符、最小宽度限定符

--------------------------------------------------------------------------------------

Android 版本适配
Android 13(33)
细分媒体权限:将 READ_EXTERNAL_STORAGE 细分为IAMGES、VIDEO、AUDIO权限
静态广播注册:注册静态广播时,需设置对其他应用的可见性
Wi-Fi :新增 NEARBY_WIFI_DEVICES 运行时权限

TargetSdkVersion >=34的修改
对于以 Android 14 为目标平台的应用,Android 会通过以下方式限制应用向内部应用组件发送隐式 intent:
隐式 intent 只能传送到导出的组件。应用必须使用显式 intent 传送到未导出的组件 带包名的去跳转,或将该组件标记为已导出。
如果应用通过未指定组件或软件包的 intent 创建可变待处理 intent,系统现在会抛出异常。

在运行时注册的广播接收器必须指定导出行为

------------------------------------------------------------------------------------------


序列化Serializable/Parcelable
S只起到了一个标识的作用,用于告知程序实现了Serializable的对象是可以被序列化的,但真正进行序列化和反序列化的操作是通过ObjectOutputStream及ObjectInputStream实现的。
P序列化过程中会用到Parcel,Parcel可以被认为是一个包含数据或者对象引用的容器,能够支持序列化及在跨进程之后的反序列化。
P的序列化操作在Native层实现,通过write内存写入及read读内存数据重新生成对象。P将对象进行分解,且分解后每一部分都是支持可传递的数据类型。

------------------------------------------------------------------------------------------

java线程
线程间通信:共享数据,同步锁,阻塞队列
wait():令当前线程挂起并放弃cpu、同步资源并等待,使别的线程可以访问并修改共享资源。当前线程等待其他线程调用notify()方法唤醒。
notify():唤醒正在等待的线程()
notifyAll():唤醒正在等待的所有线程()

sleep()作用期间,该线程不占用CPU资源,但是不会释放锁。sleep是由硬件提供的延时,如果要中断sleep,也需要通过相关的特殊功能寄存器进行操作。
join():令当前线程强行插入执行,如果指定的线程已经线束,那么它会立即返回

yield和sleep方法
Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。
实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

stop()方法终止线程(不安全)
interrupt方法结束线程
线程处于阻塞状态:如使用了sleep(),同步锁的wait(),socket中的receiver(),accept()等方法时,会使线程处于阻塞状态。当调用线程interrupt()方法时,会抛出InterruptException异常。
阻塞中的那个方法抛出此异常,通过代码可以捕获此异常,然后跳出循环状态,从而让我们有机会结束这个线程的执行。
并不是只要调用interrupt()方法,线程就会结束,实际上是不正确的,一定要先捕获InterruptException异常之后通过break来跳出循环,才能正常结束run()方法。

------------------------------------------------------------------------------------------

抽象类与接口的区别:
抽象类和接口都不能北实例化。
接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现。接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

static 关键字
可以修饰成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属于某个对象的。
随着类的加载而加载
优先于对象存在
修饰的成员,被所有对象所共享
访问权限允许时,可不创建对象,直接被类调用
静态块在全过程之中,只有在加载类的时候加载一次,其他时候都不会调用了

final
修饰变量:为常量,值不能改变
修饰对象:值能够改变,而引用不能改
修饰方法:不能够重写
修饰类:不可以被继承


Exception与Error有什么区别
1、Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
2、Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。


什么是反射机制
Java反射机制是指在运行时动态地获取一个类的信息并能够操作该类的属性和方法的能力。Java反射机制使得程序能够在运行时借助Class类的API来操作自身的属性和方法,从而大大增强了Java的灵活性和可扩展性

Java反射机制的原理主要是通过Class类来实现的。Class类是Java中反射机制的核心类,它可以在运行时动态地获取一个类的信息
Class类:它是反射机制的核心类,通过它可以获得类的属性和方法,实例化对象等。Java中每个类都有一个与之对应的Class对象。
Constructor类:它代表类的构造函数,可以用来创建实例对象。
Method类:它代表类的方法,可以用来执行类的方法。
Field类:表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)

什么是泛型
泛型,即参数化类型,目的是将具体类型参数化,在使用时需要传入具体类型进行替换。

2、为什么需要使用泛型?
https://blog.csdn.net/swadian2008/article/details/100546069
1)保证类型安全,进行编译期错误检查,使代码具有更好的安全性和可读性。
2)不需要进行类型强制转换。

什么是类型擦除?
Java 泛型的实现是在编译层,编译后生成的字节码中不包含泛型中的类型信息。所以使用泛型时,加上的类型参数,会在编译器编译的时候去掉,这个过程称为类型擦除。
擦除后只会保留原始类型:原始类型(raw type)就是擦除了泛型信息,最后在字节码中的类型,原始类型才是变量的真正类型。
无限定类型的擦除,原始类使用 Object 替换
限定类型的擦除,原始类型用边界的类型替换

类型擦除带来的问题:
泛型中参数化类型不考虑继承关系

类型擦除与多态冲突
原本是子类对父类方法进行重写,实现多态。但类型擦除以后,就变成了重载(形参、反参不一致),这样类型擦除就和多态有了冲突。

不能抛出也不能捕获泛型类的对象
原因:异常都是在运行时捕获和抛出的,编译后,泛型类型信息被擦除,会导致catch两个一模一样的普通异常,这个是不允许的,因此编译会报错。

-------------------------------------------------------------------------------------------------------------

泛型

泛型类、泛型接口、泛型方法。

泛型类中的泛型方法,泛型方法的定义优先于泛型类

限定类型参数:T extends A&B

泛型的局限:

泛型不能实例化;泛型不能在静态域和静态方法中使,但是静态的泛型方法可以;类型擦除;泛型不能是8种基础类型

泛型可以定义为数组,但是实例化会报错;泛型类不能派生自Throwable/Exception;

泛型方法可以派生自Throwable,但是不能被捕获,但是可以抛出

public <T extends Throwable> void doWork(T t){

    try{

    }cache(T t){ //编译报错

    }

}

-----------------------------------------------------------------

public <T extends Throwable> void doWork(T t) throws T{

    try{

    }cache(Throwable e){ //编译报错

        throw t

    }

}

-------------------------------------------------------------------------------------------------------------


MVVM
Model(模型):负责处理数据和业务逻辑。它可以是从网络获取的数据、数据库中的数据或其他数据源。Model层通常是独立于界面的,可以在多个界面之间共享。
View(视图):负责展示数据和与用户进行交互。它可以是Activity、Fragment、View等。View层主要负责UI的展示和用户输入的响应。
ViewModel(视图模型):连接View和Model,作为View和Model之间的桥梁。它负责从Model中获取数据,并将数据转换为View层可以直接使用的形式。
ViewModel还负责监听Model的数据变化,并通知View进行更新。ViewModel通常是与View一一对应的,每个View都有一个对应的ViewModel。


MVVM的特点和优势
解耦合:MVVM通过将View和Model解耦合,使得它们可以独立开发和测试。ViewModel作为中间层,将数据从Model传递给View,避免了直接在View中处理业务逻辑的情况。
可维护性:MVVM的分层结构使得代码更易于维护。View只负责展示数据和用户交互,ViewModel负责处理业务逻辑和数据转换,Model负责数据的获取和存储。这种分离使得代码更加清晰和可读,也方便进行单元测试。
数据驱动UI:MVVM采用数据绑定的方式,将Model的数据与View进行绑定。当Model的数据发生变化时,ViewModel会自动更新View的显示,无需手动更新UI。这种方式可以减少手动更新UI的代码量,提高开发效率。


RxJava2 背压详解
对于异步订阅关系,存在 被观察者发送事件速度 与观察者接收事件速度 不匹配的情况,被观察者 发送事件速度太快,而观察者 来不及接收所有事件,从而导致观察者无法及时响应 / 处理所有发送过来事件的问题,
最终导致缓存区溢出、事件丢失 & OOM,这个时候就需要使用到背压了。
Flowable 解决背压问题
ERROR类型:当被观察者发送事件大于128时,观察者抛出异常并终止接收事件,但不会影响被观察者继续发送事件
DROP类型:每当观察者接收128事件之后,就会丢弃部分事件
LATEST:LATEST与DROP使用效果一样,但LATEST会保证能接收最后一个事件,而DROP则不会保证

Flow的背压机制
在Flow中,不同于一些传统的响应式框架,它的背压管理是使用Kotlin挂起函数suspend实现的
1.使用buffer进行缓存收集
当Buffer溢出的时候,它为我们提供了三个选项
默认使用SUSPEND:会将当前协程挂起,直到缓冲区中的数据被消费了
DROP_OLDEST:它会丢弃最老的数据
DROP_LATEST: 它会丢弃最新的数据

2.使用conflate解决


CI/CD
CI/CD是持续集成(Continuous Integration)和持续交付(Continuous Delivery)的缩写,它旨在通过自动化的流程和工具,提高软件开发的效率、质量和交付速度。

Kotlin之高阶函数
高阶函数的定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数称为高阶函数
高阶函数允许让函数类型参数决定函数的执行逻辑,即使同一个高阶函数,传入的函数类型参数不同,那么函数的执行逻辑和返回结果可能也是完全不同的。
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
    return block(num1, num2)
}
fun plusFunc(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minusFunc(num1: Int, num2: Int): Int {
    return num1 - num2
}

同文件下
num1AndNum2(20, 30, ::plusFunc)
num1AndNum2(20, 30, ::minusFunc)
不同文件下
val highFuncTest: HighFuncTest = HighFuncTest()
num1AndNum2(20, 30, highFuncTest::plusFunc)
num1AndNum2(20, 30, highFuncTest::minusFunc)

Lambda表达式
val plusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 + n2 }
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Lambda表达式的最后一行代码的返回值作为函数的返回值返回。

我们知道Kotlin代码最终会编译成Java字节码的,而Java中是没有高阶函数概念的,其实Kotlin编译器最终会把Kotlin中高阶函数的语法转换成Java支持的语法结构。
转换成java后,函数参数会转化成Function接口,这是Kotlin的内置接口,里面有一个待实现的invoke()函数。
这就是高阶函数背后的原理,原来传入的Lambda表达式在底层被匿名类所代替,这也就说明我们每调用一次Lambda表达式就会创建一个匿名类对象,当然会带来额外的内存和性能开销。


内联函数的使用以及原理
内联函数的使用比较简单就是在定义的高阶函数时加上inline关键字即可。
inline fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
    return block(num1, num2)
}

内联函数的原理也很简单:Kotlin编译器在编译时把内联函数内代码自动替换到要调用的地方,这样就解决了运行时的内存开销。


Compose:万物皆函数
setContent{

}

@Composable
Text
Spacer  
Image
column
row


kotlin委托(代理模式,和java代理不同的是:kotlin委托角色不用重写接口约束,但可以调用被委托类的方法。减少模版代码)
委托模式中,有三个角色,约束、委托对象和被委托对象
在Kotlin中将委托功能分为两种:类委托和属性委托。

在Kotlin中,委托用关键字by,by后面就是委托的对象,可以是一个表达式
class HouseOwner(private val agent: IRentHouse): IRentHouse by agent {
    // 签合同
    fun sign() {
        println("房主签合同")
    }
    // 带人看房
    override fun showHouse() {
        println("房主带人看房")
    }
}

属性委托
属性委托是将属性的get/set方法的逻辑委托给一个类进行实现。   
var prop: Any by Delegate()


Kotlin标准库的几种委托
延迟属性 lazy

可观察属性
Kotlin除了提供了lazy函数实现属性延迟加载外,还提供了Delegates.observable和Delegates.vetoable标准库函数来观察属性变化

var observableProp: String by Delegates.observable("初始值") { property, oldValue, newValue ->
    println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
}
// 测试
fun main() {
    observableProp = "第一次修改值"
    observableProp = "第二次修改值"
}

vetoable函数
vetoable函数与observable一样,都可以观察属性值变化,不同的是,vetoable可以通过代码块逻辑决定属性值是否生效
var vetoableProp: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
    println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
    newValue > 0
}
// 测试
fun main() {
    println("vetoableProp:$vetoableProp")
    vetoableProp = 2
    println("vetoableProp:$vetoableProp")
    vetoableProp = -1
    println("vetoableProp:$vetoableProp")
    vetoableProp = 3
    println("vetoableProp:$vetoableProp")
}


挂起函数suspend
挂起函数只能从协程或其他挂起函数中调用。
挂起函数与普通函数的区别在于:挂起函数可以挂起和恢复。挂起和恢复也是协程与线程相比的优势。
挂起函数的特点是使用同步的方式完成异步任务。
挂起函数的本质就是 Callback。Kotlin 的编译器会将 suspend 函数转换为带有 Callback 的普通函数。


Lambda表达式
()-> Unit
(Int) -> Any

val method : ()-> Unit
method = {printlin("")}
调用:method() 或method.invoke()

val method2 = {"我是方法2"} //会把string当做返回值
println(method2())

val method3 = {n1:Int,n2:Int -> n1 + n2} 

val method4 : (Int) -> string
method4 = fun(value:Int):String = value.toString()
println(method4(77))

val method5 : (String,String) -> Unit = {st1,st2 -> println(st1 + st2)}

fun t1 {true} //返回值为Unit的函数

函数返回函数
fun s1 = {} 
fun s1 : ()->Unit = {}
fun s2 : ()->Boolean = {true}
调用:s2()() //双括号

kotlin高阶函数
返回string类型。lambda可以定义为任意字段(aaa)
fun show1(number: Int,lambda:(Int)->String) = lambda(number)
使用:var s = show1(99){"返回数字:$it"} //最后一个函数参数可以后缀{}表示


扩展:对T扩展,T本身就==this 
//给泛型T增加匿名扩展函数 mm。本质是lambda函数,但是调用者是T,可以让具体实现通过this持有T实例
fun <T> T.myRunOk(mm:T.(Float) -> Boolean) = mm(1.2f)
fun <T> T.myRunOk(mm:T.(Float) -> Boolean): () -> Boolean = { mm(123.2f) }


线程和协程:
协程由编程者控制,协程之间可以有优先级;
线程由系统控制,一般没有优先级。线程是CPU最小的调度单位。
协程几乎比线程快一个数量级。协程调用由编码者控制,可以减少无效的调度
协程可以控制内存占用量,灵活性更好;线程是由系统控制
协程使用同步的代码,写出异步的效果

开启协程:
GlobalScope.launch{}
runBlocking {}
job= MainScope().launch{}//在activity中使用
viewModelScope.launch{}//需要在viewmodel中使用
lifecycleScope.launch{}
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    ...
}

Deferred = async{}


协程原理:
协程是通过使用挂起函数来实现的。
suspend函数会被编译成带有Continuation类型参数的函数。本质上是一个回调。
状态机


jetPack
1、lifecycle,一个activity可以添加多个。
用法 lifecycle.addObserver(LifecycleObserver)
原理 ComponentActivity中会持有一个LifecycleRegistry用来存储LifecycleObserver,一个ReportFragment,用来触发回调。

2、LiveData:需要配合Viewmodel使用,具有生命周期感知能力,这种感知能力确保只更新活跃状态的组件观察者,并只更新最后一个数据。
Livedata可以在object{}中实现全局变量并更新。
LiveData<String> livedata = new LiveData<String>()
livedata.setValue()
livedata.postValue()
livedata.observe(this, new Observer<String>() {
            @Override
            public void onChanged(String dataBeans) {
            }
        });


Jetpack 引入了 Lifecycle,让任何组件都能方便地感知界面生命周期的变化。只需实现 LifecycleEventObserver 接口并注册给生命周期对象即可。
LiveData 的数据观察者在内部被包装成另一个对象(实现了 LifecycleEventObserver 接口),它同时具备了数据观察能力和生命周期观察能力。

LiveData 是如何避免内存泄漏的?
LiveData 内部会将数据观察者进行封装,使其具备生命周期感知能力。当生命周期状态为 DESTROYED 时,自动移除观察者。

LiveData 是粘性的吗?若是,它是怎么做到的?
LiveData 的值被存储在内部的字段中,直到有更新的值覆盖,所以值是持久的。
两种场景下 LiveData 会将存储的值分发给观察者。一是值被更新,此时会遍历所有观察者并分发之。二是新增观察者或观察者生命周期发生变化(至少为 STARTED),此时只会给单个观察者分发值。
LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值。该值的初始值是-1,每次更新 LiveData 值都会让版本号自增。
LiveData 并不会无条件地将值分发给观察者,在分发之前会经历三道坎:1. 数据观察者是否活跃。2. 数据观察者绑定的生命周期组件是否活跃。3. 数据观察者的版本号是否是最新的。
“新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发。


3、DataBinding
可以继承BaseObservable,也可以用ObservableField<T>包裹。
可以自定义属性,用于给imageVIew,ListView等更新UI
<android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:bindData="@{data}"/>

@androidx.databinding.BindingAdapter("bind:bindData")
public static void setAdapterAndData(RecyclerView recyclerView, List<String> data) {
    recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
    recyclerView.setAdapter(new BindingAdapter(recyclerView.getContext(), data));
}

也可以设置观察者,在监听回调里更新UI
ObservableArrayList<Entity> mList = new ObservableArrayList<>();
wanViewModel.mList.addOnListChangedCallback(new ObservableList.OnListChangedCallback(){})

4、Viewmodel
横竖屏切换,数据不会丢失

room数据库使用  APT技术
创建实体
@Entity
class Student
创建表操作接口
@Dao
interface StudentDao
创建数据库
@Database(entities = [Student::class], version = 1, exportSchema = false)
abstract class AppDataBase() : RoomDatabase(){
    companion object {
        val instance: AppDataBase = Room.databaseBuilder(
            GlobalApp.mApplication,
            AppDataBase::class.java,
            "DerryDb"
        ).build()

    }

    abstract fun studentDao(): StudentDao
}


枚举和密封类的区别
1、枚举中每个枚举的属性和函数必须是相同的,密封类中每个子类型可以有自己的属性和函数。
2、枚举不能被继承,密封类可以被继承。
3、枚举类不能继承其他类,它们只能继承接口。密封类可以继承其他类以及接口。
4、密封类可以让我们在使用when表达式时,不需要写else分支,因为所有的情况都已经覆盖了,这样可以减少代码的冗余和错误。

枚举不会被垃圾收集,它们会在您的应用程序的整个生命周期中保留在内存中。这可能是有利的,也可能是不利的。


如何区分挂起函数

rxjava原理 

rxjava操作符

okhttp设计模式  

okhttp拦截器

retrofit使用

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值