fun main() {
simple().forEach { value -> println(“receiver$value”) }
println(“end”)
}
结果
send1
receiver1
send2
receiver2
send3
receiver3
end
For example 3 : 异步计算并返回?引入flow
有什么办法可以异步计算多个值并且返回的吗?当然可以,我们可以使用挂起函数,使计算过程在异步线程执行,最终以list的形式返回。
举个🌰
suspend fun simple(): List {
//模拟耗时操作
delay(1000)
return listOf(1, 2, 3)
}
fun main()= runBlocking {
val job = launch {
simple().forEach { value -> println(value) }
}
launch {
println(“other operate”)
}
job.join()
}
结果:从结果来看,耗时操作并没有影响主线程的运行😎
other operate
1
2
3
但是,这样就够了吗?no no no !🙅♀️
使用list
意味着我们只能一次性的返回所有的值。所以为了表示流的计算,引入了flow
。就像可以使用Sequence
类型用于同步计算值一样。
Flow 简单使用
上面介绍了flow
要解决什么问题,那么我们就开始使用起来吧。
先看一个简单的🌰
fun simple(): Flow = flow { // flow 构建
for (i in 1…3) {
//模拟异步耗时计算
delay(100)
//发射值
emit(i)
}
}
fun main() = runBlocking {
// launch 一个协程 同时延时100毫秒打印 校验主线程是否阻塞
launch {
for (k in 1…3) {
println(“I’m not blocked $k”)
//主线程在这个时间段可以干别的事情
delay(100)
}
}
// collect flow value
simple().collect { println(it) }
}
结果:通过线程打印 I'm not blocked
证明异步计算不会阻塞主线程,计算成功之后会resume到collect里面继续执行。
I’m not blocked 1
1
I’m not blocked 2
2
I’m not blocked 3
3
通过上述代码,需要注意一下几点:🙆♀️
-
使用
flow
代码块构建出来的类型为Flow
-
flow
代码块里面允许写挂起函数。比如上面的,delay
emit
。 -
使用
emit
进行值的发射,使用collect
进行值的收集
Flow 构建
除了上面使用的flow{}
进行构建之外,还可以使用其他的方式进行构建。
- 使用
flowOf
可以定义一组固定的值
fun simple(): Flow = flowOf(1, 2, 3)
- 可以使用
asFlow()
扩展函数将各种集合和序列转换为流。
// 将list转换为flow
listOf(1,2,3).asFlow().collect { value -> println(value) }
Flow 冷流
Flow是冷流,构建器代码在调用collect之前是不会进行调用的,对于多个调用者,都会重新走一遍构建器的代码。
废话不多说,上🌰
fun simple(): Flow = flow {
println(“Flow started”)
for (i in 1…3) {
delay(100)
emit(i)
}
}
fun main() = runBlocking {
println(“Calling simple function…”)
val flow = simple()
println(“Calling collect…”)
flow.collect { value -> println(value) }
println(“Calling collect again…”)
flow.collect { value -> println(value) }
}
结果
Calling simple function…
Calling collect…
Flow started
1
2
3
Calling collect again…
Flow started
1
2
3
每次收集流时都会开始,这就是为什么我们再次调用 collect 时会看到“Flow started”的原因。
Flow 取消
如何取消一个Flow呢?
Kotlin官方并没有提供flow取消的函数。啊 这???😕听到这个是不是还满疑惑。且听我细细道来。
Flow需要在协程里面使用,因为collect
是挂起函数,另外基于冷流的特性,不调用collect
构建器的代码压根不会走。所以只能是协程。那 我取消协程不就行了吗?😮。好像之前有看到过有开发者提出过,是否要给flow单独加一个取消的函数,被Jetbrains无情的拒绝了,哈哈哈哈很搞笑。下面引用Kotlin官方的一段话。
Flow adheres to the general cooperative cancellation of coroutines. As usual, flow collection can be cancelled when the flow is suspended in a cancellable suspending function (like delay).
adheres 坚持
Flow 坚持协程的一般协作取消。 像往常一样,当流在可取消的挂起函数(如延迟)中被挂起时,可以取消流收集。
这个adheres
好像就像是在回复广大的开发者,你取消协程就行了😂😂😂。
好了,下面看取消的🌰
fun simple(): Flow = flow {
for (i in 1…3) {
delay(100)
println(“emit $i”)
emit(i)
}
}
fun main() = runBlocking {
val job = launch {
simple().collect { println(it) }
}
delay(250)
job.cancel(CancellationException(“timeout 250”))
println(“done”)
}
结果:看我们只需要取消对应的协程即可,对应的flow也会被取消收集。
emit 1
1
emit 2
2
done
这里引申一点,对于timeout,官方有提供专用的操作函数,withTimeout系列。不需要我们手动delay然后继续调用取消,毕竟不是很优雅。
上述代码也可以写成如下的形式
fun simple(): Flow = flow {
for (i in 1…3) {
delay(100)
println(“emit $i”)
emit(i)
}
}
fun main() = runBlocking {
withTimeoutOrNull(250) {
simple().collect { value -> println(value) }
}
println(“done”)
}
看到了吗?直接将launch替换为withTimeoutOrNull
就可以做到延时取消效果了,这里简单做一下源码分析。
源码
public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
…
try {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
coroutine = timeoutCoroutine
setupTimeout<T?, T?>(timeoutCoroutine, block)
}
} catch (e: TimeoutCancellationException) {
…
}
}
private class TimeoutCoroutine<U, in T: U>(
@JvmField val time: Long,
uCont: Continuation // unintercepted continuation
) : ScopeCoroutine(uCont.context, uCont), Runnable {
override fun run() {
cancelCoroutine(TimeoutCancellationException(time, this))
}
…
}
private fun <U, T: U> setupTimeout(
coroutine: TimeoutCoroutine<U, T>,
block: suspend CoroutineScope.() -> T
): Any? {
// schedule cancellation of this coroutine on time
coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context))
…
}
public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
DefaultDelay.invokeOnTimeout(timeMillis, block, context)
省略了一大堆非核心的代码,我们直接看延时取消的操作,这里简单分析一下:
-
创建TimeoutCoroutine对象,它同时实现了Runnable,在run里面调用了取消函数,抛出TimeoutCancellationException。
-
调用当前context的invokeOnTimeout函数,该函数需要一个Runnable,传入了timeoutCoroutine。此实现使用内置的单线程调度执行器服务。会在延时对应的事件后调用Runnable的run函数,然后就会取消当前的协程。
-
在取消协程之后,会取消掉当前上下文的所有将在完成时调用的回调,disposeOnCompletion函数被调用。
Flow 相关操作符
这一块的操作符,其实是比较多的。但是如果您熟悉RxJava的话,其实都是差不多的。这一块这里就不做源码分析了,只是看一下怎么使用即可。内部其实是创建了新的流返回出来了,有兴趣的话可以自行查看一下源码。
中间流操作符
map
映射。看🌰(传入请求流可以使用 map 操作符映射到结果,即使执行请求是由挂起函数实现的长时间运行的操作)
suspend fun performRequest(request: Int): String {
delay(1000) // 模拟长时间的异步工作
return “response $request”
}
fun main() = runBlocking {
(1…3).asFlow() // 转换为flow
.map { request -> performRequest(request) }
.collect { response -> println(response) }
}
结果
response 1
response 2
response 3
filter
过滤操作,看🌰
suspend fun performRequest(request: Int): String {
delay(1000) // 模拟长时间的异步工作
return “response $request”
}
fun main() = runBlocking {
(1…3).asFlow() // 转换为flow
.map { request -> performRequest(request) }
.filter { it == “response 1” }
.collect { response -> println(response) }
}
结果:仅返回匹配到的值
response 1
变换操作符
在流变换算子中,最通用的一种叫做变换。 它可以用来模仿简单的转换,比如 map 和 filter,也可以实现更复杂的转换。 使用transform
,我们可以发出任意次数的任意值。
例如🌰,使用transform
,我们可以在执行长时间运行的异步请求之前发出一个字符串,并在其后响应:
(1…3).asFlow() // a flow of requests
.transform { request ->
emit(“Making request $request”)
emit(performRequest(request))
}
.collect { response -> println(response) }
结果
Making request 1
response 1
Making request 2
response 2
Making request 3
response 3
size限制操作符
顾名思义,限制收集的数量。使用运算符take
它会在判断当发射的值在达到相应限制时取消流程的执行。 因为协程中的取消总是通过抛出异常来执行。所以需要考虑进行相应的异常捕获来保证后续的流畅正常进行不被取消掉。
fun numbers(): Flow = flow {
try {
emit(1)
emit(2)
println(“not execute”)
emit(3)
} finally {
println(“finally in numbers”)
}
}
fun main() = runBlocking {
numbers()
.take(2) // take 两个值
.collect { value -> println(value) }
}
结果
1
2
finally in numbers
终端操作符
终端操作符可以启动一个流,最基础的就是上述常提到的collect
。但是还有一些其他的终端操作符,它可能会让一些操作变得更简单:
转换为各种集合, toList 和 toSet。
flowOf(1,2).toList().forEach {
println(it)
}
flowOf(1,2).toSet().forEach(::println)
first , 确保获取且进获取第一个值
返回流发出的第一个元素然后取消流的集合的终端运算符。 如果流为空,则抛出 NoSuchElementException。
val value : Int = flowOf(1, 2).first()
println(value)
使用 reduce 和 fold 将流合并到一个值。
val sum = (1…5).asFlow()
.map { it * it } //平方
.reduce { a, b -> a + b } // 进行累加
println(sum)
//结果
55
fold和reduce使用起来差不多,区别就是fold可以定义初始化,其实很简单,reduce传入的lambda前一个参数是每次计算的结果累计,后一个参数是当前需要传入的值,不明白可以去瞅一眼源码,这里不在引申。
onEach
这个操作符也较为常用,这里也介绍一下,返回在上游流的每个值向下游发出之前调用给定操作的流。
🌰
(1…5)
.asFlow()
.onEach {
println(“onEach$it”)
}.collect()
//结果
onEach1
onEach2
onEach3
onEach4
onEach5
操作符的顺序
除非使用对多个流进行操作的特殊运算符,否则流的每个单独集合都按顺序执行。 该集合直接在调用终端运算符的协程中工作。 默认情况下不会启动新的协程。 每个发出的值都由从上游到下游的所有中间操作符处理,然后交付给终端操作符。
🌰
(1…5).asFlow()
.filter {
println(“Filter $it”)
it % 2 == 0
}
.map {
println(“Map $it”)
“string $it”
}.collect {
println(“Collect $it”)
}
//结果 按照顺序没有值依次向下发射
Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5
Flow 调度器切换
对于UI驱动型的程序来说,需要将长时间计算的任务放在异步线程处理,UI展示工作需要放在主线程处理。也就是说需要将构建器的代码放到异步线程执行,但是终端操作符,比如collect
需要在主线程获取,那么怎么做呢?
使用flowOn
操作符
在这之前您可能的了解一下,协程的调度器。可以简单参考之前写的一篇文章,有对调度器做简单介绍:https://blog.csdn.net/weixin_44235109/article/details/119981210
fun main() = runBlocking {
flow {
for (i in 1…3) {
//模拟异步处理
delay(100)
log(“Emitting $i”)
emit(i) // emit next value
}
}.flowOn(Dispatchers.Default)//使用flowOn传入Default的调度器
.collect { value ->
log(“Collected $value”)
}
}
结果
16:39:21:954 [DefaultDispatcher-worker-1] Emitting 1
16:39:21:969 [main] Collected 1
16:39:22:071 [DefaultDispatcher-worker-1] Emitting 2
16:39:22:071 [main] Collected 2
16:39:22:178 [DefaultDispatcher-worker-1] Emitting 3
16:39:22:178 [main] Collected 3
可以很明显的看出,构建模块被调度到异步线程处理了。而收集的工作还在主线程进行。
flowOn
负责构建的模块调度,那么收集的谁负责呢?
其实和异常处理类似,collect受调用它的协程上下文限制,所以最后的执行线程和当前协程上下文的调度器有关,目前我使用的是idea
测试的,默认runBlocking
的调度器就是主线程。如果是android上面的话,runBlocking
可能就需要传入Dispatchers.Main
了。
其实和RxJava还是非常相似的😂。
注意一点,此时其实已经改变流执行的顺序了。
官方的解释如下:
Another thing to observe here is that the flowOn operator has changed the default sequential nature of the flow. Now collection happens in one coroutine (“coroutine#1”) and emission happens in another coroutine (“coroutine#2”) that is running in another thread concurrently with the collecting coroutine. The flowOn operator creates another coroutine for an upstream flow when it has to change the CoroutineDispatcher in its context.
这里要注意的另一件事是 flowOn 运算符更改了流的默认顺序性质。 现在收集发生在一个协程(“coroutine#1”)中,发射发生在另一个协程(“coroutine#2”)中,该协程与收集协程同时运行在另一个线程中。 当必须在其上下文中更改 CoroutineDispatcher 时, flowOn 运算符为上游流创建另一个协程。
这一块我简单看了一下源码,这里面不同的调度器会遇到多线程的问题,最里面使用了channel进行了调度处理。具体的核心类是ChannelFlow
。后面会对flow
进行简单源码分析,但篇幅有限不对这一块过深入分析,感兴趣可以自行查看,或找博主私下探讨。
其实上面的看不出来会改变流执行的顺序,下面改变一下代码,验证一下看看🌰
fun main() = runBlocking {
flow {
for (i in 1…3) {
//模拟异步处理
delay(100)
log(“Emitting $i”)
emit(i) // emit next value
}
}.flowOn(Dispatchers.Default)
.collect { value ->
delay(200)
log(“Collected $value”)
}
}
结果
16:52:33:258 [DefaultDispatcher-worker-1] Emitting 1
16:52:33:386 [DefaultDispatcher-worker-1] Emitting 2
16:52:33:482 [main] Collected 1
16:52:33:493 [DefaultDispatcher-worker-1] Emitting 3
16:52:33:684 [main] Collected 2
16:52:33:887 [main] Collected 3
我们只需要将,collect里面增加一个delay即可,发现其实这时候就是发射归发射,收集归收集了。不似上面我们写的程序,发射一个值只有到终端操作符之后才会发射第二个。这里面肯定就会对值进行缓存。那么就会牵扯到一个问题。老生常谈🙆♀️,背压处理!
Flow 背压处理
对于背压处理,Kotlin 提供三种解决方案:
| 操作符 | 含义 |
| — | — |
| buffer | 指定固定容量缓存 |
| conflate | 保留最新的值 |
| collectLatest | 新值发送时取消之前的 |
buffer
这里有必要看一下buffer函数的源码定义
public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND)
可以看出需要两个参数,都有默认值。
-
第一个好理解容量,默认等于BUFFERED,这个值其实时64,可以按照使用时需要自己指定具体的数字。
-
第二个是指定,当buffer溢出时的操作,默认的操作是挂起。还要两个操作分别是删除最旧的值不要挂起或者删除当前最新的值不要挂起,可以自行查看源码,这里不再引申。
使用🌰
flow {
for (i in 1…3) {
//模拟异步处理
delay(100)
log(“Emitting $i”)
emit(i) // emit next value
}
}.flowOn(Dispatchers.Default)
.buffer()
.collect { value ->
delay(200)
log(“Collected $value”)
}
//结果
17:17:28:420 [DefaultDispatcher-worker-1] Emitting 1
17:17:28:536 [DefaultDispatcher-worker-1] Emitting 2
17:17:28:646 [main] Collected 1
17:17:28:646 [DefaultDispatcher-worker-1] Emitting 3
17:17:28:846 [main] Collected 2
17:17:29:049 [main] Collected 3
conflate
这个只获取最新值也比较好理解,应用场景,比如说获取下载进度,对于用户来说其实每次只需要获取当前最新的进度就好了,不需要把之前的值再去获取一遍,下面也举一个例子🌰
fun main() = runBlocking {
flow {
for (i in 1…3) {
//模拟异步处理
delay(100)
log(“Emitting $i”)
emit(i) // emit next value
}
}.flowOn(Dispatchers.Default)
.conflate()
.collect { value ->
delay(300)//模拟下游处理比较慢
log(“Collected $value”)
}
}
//结果 第一个值肯定可以拿到 当地一个值处理完成之后 最新的值就是3了 所以丢弃了2
17:21:42:916 [DefaultDispatcher-worker-1] Emitting 1
17:21:43:034 [DefaultDispatcher-worker-1] Emitting 2
17:21:43:140 [DefaultDispatcher-worker-1] Emitting 3
17:21:43:236 [main] Collected 1
17:21:43:546 [main] Collected 3
collectLatest
说明一点:这玩意其实也是一个终端操作符
前两个可能都比较好理解,那新值发送时取消之前的
是什么意思呢?为了便于理解,直接上例子,按照结果说明:
🌰
fun main() = runBlocking {
flow {
for (i in 1…3) {
//模拟异步处理
delay(100)
log(“Emitting $i”)
emit(i) // emit next value
}
}.flowOn(Dispatchers.Default)
.collectLatest {
delay(300)
log(“Collected $it”)
}
}
//结果
17:25:33:916 [DefaultDispatcher-worker-1] Emitting 1
17:25:34:038 [DefaultDispatcher-worker-1] Emitting 2
17:25:34:150 [DefaultDispatcher-worker-1] Emitting 3
17:25:34:453 [main] Collected 3
对比上面的例子,这里只是将collect
替换成了collectLatest
而已。为什么1
没有了呢?
显而易见了,这玩意会在最新的到来会直接取消下游上一个消费的处理,因为有delay所以1
还没有来得及打印,就因为下一个值发射了,然后就被取消了!!!您可真霸道呢?🙆♀️
Flow 异常处理
当操作符内的发射器或代码抛出异常时,流收集可以以异常结束。 有几种方法可以处理这些异常。
try…catch
较为简单,上🌰
fun simple(): Flow = flow {
for (i in 1…3) {
println(“Emitting $i”)
emit(i) // emit next value
}
}
fun main() = runBlocking {
try {
simple().collect { value ->
println(value)
check(value <= 1) { “Collected $value” }
}
} catch (e: Throwable) {
println(“Caught $e”)
}
}
//结果
fun simple(): Flow = flow {
for (i in 1…3) {
println(“Emitting $i”)
emit(i) // emit next value
}
}
fun main() = runBlocking {
try {
simple().collect { value ->
println(value)
if (value>1){
throw IllegalStateException(“exception value is $value”)
}
}
} catch (e: Throwable) {
println(“Caught $e”)
}
}
显而易见,抛出异常之后,收集结束,如果是UI驱动程序比如:Android还是推荐主从作用域,异常不会向上传播。
思考一个问题,刚刚异常是发生在收集端,如果在构建的时候发生异常呢?
fun simple(): Flow =
flow {
for (i in 1…3) {
println(“Emitting $i”)
emit(i) // emit next value
}
}
.map { value ->
if (value>1){
throw IllegalStateException(“exception value is $value”)
}
“string $value”
}
fun main() = runBlocking {
try {
simple().collect { value -> println(value) }
} catch (e: Throwable) {
println(“Caught $e”)
}
}
//结果
Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: exception value is 2
完美:异常仍被捕获并停止收集
上述代码的问题就是不够优雅,还有异常对于流来说必须是透明的,使用try … catch 显然违反了透明性,所以Kotlin 封装了try catch.
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?
Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。
以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。
最后,赠与大家一句诗,共勉!
不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
a.lang.IllegalStateException: exception value is 2
完美:异常仍被捕获并停止收集
上述代码的问题就是不够优雅,还有异常对于流来说必须是透明的,使用try … catch 显然违反了透明性,所以Kotlin 封装了try catch.
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-taRG2PLE-1712288063254)]
[外链图片转存中…(img-WKQgt0W9-1712288063254)]
[外链图片转存中…(img-Tv5Q4yP9-1712288063255)]
[外链图片转存中…(img-QsiBi2p9-1712288063255)]
[外链图片转存中…(img-KzbscVwL-1712288063255)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?
Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。
[外链图片转存中…(img-cWitSrmh-1712288063256)]
以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。
最后,赠与大家一句诗,共勉!
不驰于空想,不骛于虚声。不忘初心,方得始终。