搞定!从0开始手撸一个RxJava响应式编程框架(1)


要理解RxJava,必须要理解一个模式,就是观察者模式。

什么是观察者模式呢?

就好比搞公众号,你订阅了一个公众号,此时,你是观察者,公众号是被观察者。你订阅公众号简而言之就是你与公众号建立了一个联系,公众号更新文章后就会推送给所有订阅的人,其中也包括你。

两个角色,一个关系。

被观察者,有变化通知给订阅了的观察者。对应公众号

观察者,主动与一个被观察者建立订阅关系。对应你

那么代码上该如何实现呢?

简单实现观察者模式


创建一个观察者

class Observer {

fun change(){

println(“我是观察者”)

}

}

创建一个被观察者

class Observable {

//观察者集合

private val observerList= mutableListOf()

//订阅方法

fun subscribe(observer: Observer){

observerList.add(observer)

}

//通知所有观察者

fun notifyObserver(){

observerList.forEach {

it.change()

}

}

}

等等,你这被观察者怎么一堆东西?

我们一会再说这个里面的东西

fun main() {

//创建一个被观察者

val observable = Observable()

//创建一个观察者

val observer=Observer()

//被观察者订阅观察者

observable.subscribe(observer)

//被观察者通知观察者事情发生了改变

observable.notifyObserver()

}

看着观察者模式好像很深奥的样子,其实就是被观察者中有一个观察者的集合,订阅其实就是将观察者添加到这个集合中。

当有新消息的时候,被观察者遍历观察者集合,然后调用他们的方法即可。

RxJava中的观察者模式


RxJava中的观察者模式更简单,上面所说的可能会出现一堆的观察者,而RxJava模式中的观察者与被观察者是一对一的。怎么样,是不是更进一步降低了难度?

为了更好地理解事件驱动,我们可以用上游和下游来代替被观察者和观察者。

什么是上游和下游?

简单的来讲就是两个水管,这两个水管以一定的方式连接了起来,水管里的水从上面的水管流到了下面的水管。

也就是说上面的水管负责流水,下面的水管负责接水。水就是事件

我们一般是这么使用的

//创建上面的水管

Observable.create { emitter ->

//让水流出去

emitter.onNext(10)

} //连接两个水管

.subscribe (

//创建下面的水管

{ item ->

//上面的水流下来了,打印出来

println(item)

}

)

这是一般的使用方式,即创建一个上游,然后上游发送事件,下游去接收。

还有其他的使用方式,比如切换线程,或者对数据进行其他操作。

Observable.create { emitter ->

emitter.onNext(10)

}

.map{} //对数据进行转换

.subscribeOn()//改变上游发送的线程

.subscribe{}

这就是RxJava引以为傲的操作符,本质上这些操作符都是在上游和下游之间进行的一些操作。看完本篇你就明白操作符的本质,甚至可以自己写一些操作符了。

所以话不多说,我们开始彻底解剖RxJava,看看RxJava本质上到底是如何操作的。不用怕,没有源码,我只会用最简单的代码来实现一个具有完整功能的RxJava,让你彻彻底底的明白RxJava操作符,以及RxJava的线程切换。

手撸RxJava


从最基础开始走起,我们知道RxJava首先需要两个类,一个观察者,一个被观察者。

我们来定义这两个类

定义被观察者和观察者

//被观察者

class MlxObservable{}

//观察者

interface MlxObserver{}

有喜欢搞事的小伙伴可能会发现,这不对啊,你是不是骗我,你这个观察者不是类,而是个接口啊。

哎呀,行骗被发现了,赶紧逃~

其实也没有骗你,你想想,我们平常在使用RxJava的时候,是怎么使用的?Java中是不是需要new一个匿名内部类,Kotlin中则需要定义一个object实现接口。没错,观察者就是采用接口的。

为什么要这么做?

你想,观察者收到被观察者的通知以后是不是需要做一些改变,RxJava根本不知道你要做哪些改变啊,所以做成接口,你自己来实现。

又有人会问了,那你的意思被观察者也得做成一个接口啊,因为RxJava也不知道我要发送哪些数据啊。就像你上面的代码,被观察者不也是自己实现了一个接口?

恭喜你,都会抢答了!

没错,被观察者也应该做成接口,让开发者自己去实现发送哪些数据。RxJava也正是这样做的,不过为了感官上的统一,RxJava并没有把这个接口的名字叫做Observable,而是叫成了ObservableOnSubscribe

我个人更喜欢叫ObservableOnSubscribe为真正的被观察者。

定义一个真正的被观察者:

//真正的被观察者

interface MlxObservableOnSubscribe{}

被观察者也有了,观察者也有了,接下来我们需要思考下一步该做什么?

首先我们可以肯定的是,光有接口是不行的,我们得需要定义方法,来确定每个接口应该做什么。

那么我们应该思考这两个接口的职责。

从被观察者开始,被观察者应该有向下游发送数据的能力。什么叫向下游发送数据的能力呢?

其实很多人都会讲的很玄而又玄,其实就是持有下游的引用,调用下游的方法,简而言之就是回调。

定义上游的方法

不妨我们先定义这么一个方法:

interface MlxObservableOnSubscribe{

fun setObserver(observer:MlxObserver)//设置下游

}

我们暂且不去考虑如何去将下游添加到上游去,现在开发者只需要实现了被观察者接口,那么就会有下游的引用了,就可以调用下游的方法了。

定义下游的方法

接下来该考虑,下游该有哪些方法。熟悉RxJava的小伙伴应该知道,我们在实现观察者的时候会重写四个方法:onSubscribe,onNext,onError,onComplete。好,我们今天模仿RxJava,也来定义这四个方法:

interface MlxObserver {

fun onSubscribe()

fun onNext(item:T)

fun onError(e:Throwable)

fun onComplete()

}

可能有小伙伴对这个<T>不是很理解了。这个是泛型,onNext中发送的数据可能是String,也可能是Int,我们无法知道具体是哪个类型,所以我们定义一个泛型,相当于占位的,只需要调用的时候指定了是哪个类型,onNext就会收到哪个类型的数据。

既然下游定义了泛型,上游也得进行相应的修改了,需要增加泛型。

interface MlxObservableOnSubscribe{

fun setObserver(observer:MlxObserver)//设置下游

}

如此以来,实现了上游接口以后,就会得到下游的对象,调用下游的四个方法即可。其实可以看到,所谓的上游,下游其实很扯淡,就是设置了一个回调而已。也没什么难的。

现在上游也有了,下游也有了,那么我们该如何给上游设置一个下游呢?

还记得我们前面虚假的那个被观察者么,对对对,就是Observable对象。我们可以利用它来完成被观察者观察者这么一路下来的神奇操作。

RxJava中是用静态方法来完成上游的创建的,所以我们也搞个静态的。

接收上游对象

class MlxObservable{

//静态方法创建一个真正的被观察者

companion object{

fun create (source:MlxObservableOnSubscribe):MlxObservable{

return MlxObservable()

}

}

}

仔细分析一下代码,我们这里创建了一个静态方法用于接收一个真正的被观察者。返回了一个虚假的被观察者。

为什么要返回一个虚假的被观察者呢?这是因为RxJava后续的方法都不再是静态的了,所以我们需要得到一个对象。但其实更重要的原因是因为RxJava使用了装饰者模式,能够完成更好的功能拓展,各种各样的操作符也正是拓展之一。至于装饰者模式是什么,以及如何实现装饰者模式,我就不过多讲解了,看完本篇你应该会有所了解。

现在我们继续分析,我们现在返回了一个虚假的被观察者以后,需要借助这个虚假的观察者作为平台,将下游设置给上游。

接收下游对象

所以我们不妨在给虚假的被观察者也设置一个方法,这里需要注意的是我们既然返回的是类对象,那就不能在定义静态方法了。

class MlxObservable{

//这里接收一个下游对象,

fun setObserver(downStream: MlxObserver){

}

}

问题来了,下游是有了,上游呢?

哎呀呀,这是个问题,上面create方法确实是设置了一个上游,可是咱没变量保存啊。所以我们需要定义一个变量去保存上游。

private var source:MlxObservableOnSubscribe?=null

同时,最好在构造方法中就获得到上游:

class MlxObservable constructor(){

//上游对象

private var source:MlxObservableOnSubscribe?=null

//次构造方法,用于接收上游对象

constructor(source:MlxObservableOnSubscribe):this(){

this.source=source

}

}

既然这样,我们create方法也应修改一下:

fun create (emitter:MlxObservableOnSubscribe):MlxObservable{

return MlxObservable(emitter)

}

连接上下游

这样以来,上游下游都有了,该怎么办?当然是盘它了!在接收下游对象的时候上游肯定已经创建了,所以我们直接在这里就可以进行上下游的连接了,其实也就是将下游设置给上游

class MlxObservable{

//这里接收一个下游对象,

fun setObserver(downStream: MlxObserver){

source?.setObserver(downStream)

}

}

如此以来,我们RxJava就已经完成了。

什么??已经完成了,你怕不是在骗我小猫咪。

不信?我们来跑一下

MlxObservable.create(object :MlxObservableOnSubscribe{

override fun setObserver(emitter: MlxObserver) {

println(“上游发送数据:10”)

emitter.onNext(10)

}

})

.setObserver(object :MlxObserver{

override fun onSubscribe() {

println(“onSubscribe”)

}

override fun onNext(item: Int) {

println(“下游接收到数据:$item”)

}

override fun onError(e: Throwable) {}

override fun onComplete() {}

})

结果是这样的:

如何?上面的代码风格是不是已经是RxJava的风格了?不过就是变量名有点丑,人家的是subscribe方法名,我是setObserver方法名,不过不影响使用。我们后期再把它改过来,现在先这样为了能更好的理解。

有人会问,说你这个onSubscribe方法没调用啊。也是,不过这个方法见名思意就知道是在订阅的时候会调用,不管你发不发送数据,基于此,我们可以得出结论,我们应在设置之前调用下游的这个方法。也就是虚假的被观察者在收到下游之后,立马调用下游的这个onSubscribe方法:

class MlxObservable{

//这里接收一个下游对象,

fun setObserver(downStream: MlxObserver){

downStream.onSubscribe()

source?.setObserver(downStream)

}

}

如此一来就可以了。最简单的RxJava即宣告完成。

下面是简单的模型

流程步骤

  1. 使用虚假的被观察者的静态方法create创建一个真正的被观察者对象,然后设置到虚假的被观察者的sourece对象中

  2. 调用虚假的被观察者的setObserver方法创建一个观察者对象,并立马调用观察者的onSubscribe方法,然后将观察者设置给被观察者

  3. 真正的被观察者调用观察者的方法。

  4. 观察者收到数据

上图中真正的被观察者有两个矩形,其实是一个对象,我只不过把它抽出来更好的表示。

可是不对啊,RxJava明明那么强大,各种操作符,线程切换你这都不行啊。

没错,接下来我们先完成一个操作符再说。emmm,哪个操作符呢?

那就用我最常用的map把,比较有代表性。

自定义Map操作符

我们继续思考,如果我们定义Map操作符,它会作用于什么地方?

显而易见的是map操作符会作用与上游和下游之间,也就是他们的中间。其实这也正是装饰者模式要做的事情,即增强对象。

我们可以采用一种方式来进行这个操作,既然它在上下游之间,我们可不可以承接上游的水,然后做了想要的变换以后,再把变换之后的水放给下游呢?

在RxJava中,create方法调用之后即可调用map方法,map方法显而易见是一个类方法,不是静态方法,所以我们先定义map方法。但我们要考虑到的是,map方法结束之后仍然可以与下游进行通信,完完全全的保持不变,所以map方法返回的必定是一个虚假的被观察者对象。

有了这些方法我们就可以写出如下代码:

class MlxObservable{

fun map():MlxObservable{}

}

但是呢,MlxObservable也就是虚假的被观察者也是有泛型的。但是它应该是T类型吗?

答案不是,为什么呢?我们首先需要知道map的作用是什么,它是转换类型的,比如说一个string类型给你转换到下游之后就变成了Int类型。所以我们不能在使用T类型了,而要转换的类型我们也不知道,所以我们再次定义一个R类型。

于是,代码变成了这样:

class MlxObservable{

fun map():MlxObservable{}

}

方法是有了,我们该如何做变换呢?

RxJava当然不知道你想如何转换,所以RxJava也定义了一个接口,让你去实现接口,并且根据接口的返回类型作为下游的类型。

不明白?

就是说,你需要自己去定义转换的规则,根据你return的类型就能知道R是什么类型了。Java接口太麻烦了,我们可以使用Kotlin的高阶函数来实现这一点。

在map中我们传入一个高阶函数作为转换规则:

fun map(func:(T)->R):MlxObservable{

}

func:(T)->R 就是一个高阶函数,它本质上和Java的单方法的接口是一样的。这个参数func是一个函数,函数的参数是一个T类型,返回值是R类型。就这么简单。

OK,转换规则有了,我们该如何应用转换,并将转换后的数据传给下游呢?

其实很简单,熟悉装饰者模式的小伙伴应该已经知道了,不知道的也没关系。

我们接下来要做的事情就是再定义一个map自己的真正的被观察者,用于承接上游和通知下游。不明白没关系,我们先这样做,你一会就明白了。

class MlxMapObservable <T, R>():MlxObservableOnSubscribe{}

这里可能小伙伴会不明白,为什么要定义两个泛型。还记得map方法中我们又定义了一个R类型吗?为什么要定义那个R类型呢?因为已经有个T类型了,还需要一个不确定的类型,所以定义成R了。

T类型就是上游发射的类型,R类型就是要转换之后的类型。

这个类继承了MlxObservableOnSubscribe接口,这个接口就是真正的被观察者。这么做的目的是什么呢?

其实很简单,上面说了,map既然是在上下游之间,所以它既要承接上游,又要传递数据给下游。所以它自己做一个真正的被观察者,下游去观察它,它再去观察上游,上游发送数据以后,map首先收到数据,然后应用转换规则将转换后的数据在传递给下游,也就是调用下游的方法。

还不明白?

在之前的模型中,下游是直接将自己设置给了上游,而上游是直接调用下游的方法。

现在的模型中,是下游将自己设置给了map,map又将自己设置给了上游,上游依然是调用下游的方法,不过此时上游的下游不再是真正的下游了,而成为了map,当上游调用了下游的方法其实是调用了map的方法,map方法收到消息之后,应用转换,然后再次调用真正的下游。

说的太多你们可能也不大明白,我不如写出来,你们就能立马明白了。

既然MlxMapObservable实现了MlxObservableOnSubscribe接口,那么它应该是这样

class MlxMapObservable <T, R>():MlxObservableOnSubscribe{

override fun setObserver(downStream: MlxObserver){

//此时的downStream就是真正的下游

}

}

此时我们既然有了下游,我们是不是也应该获得上游和转换规则啊。

说的真对

我们此时就来把这两个写在构造方法中:

class MlxMapObservable <T, R>(

private val source:MlxObservableOnSubscribe,

private val func:((T)->R)

):MlxObservableOnSubscribe{

override fun setObserver(downStream: MlxObserver){

//此时的downStream就是真正的下游

}

}

此时,我们上游source,转换规则func,下游downStream都有了。我们该做一些事情了,在前面说到,map应该将自己设置给上游,可是map是一个被观察者啊,上游接收的是一个观察者。

所以我们需要在map自己定义一个观察者,用于接收上游传下来的数据。

class MlxMapObservable <T, R>(

private val source:MlxObservableOnSubscribe,

private val func:((T)->R)

):MlxObservableOnSubscribe{

override fun setObserver(downStream: MlxObserver){

//此时的downStream就是真正的下游

}

class MlxMapObserver<T,R>(

private val downStream:MlxObserver,

private val func:((T)->R)

):MlxObserver{

override fun onSubscribe() {

downStream.onSubscribe()//当接收到上游传来的订阅后,将事件传递给下游

}

override fun onNext(item: T) {

//应用转换规则,得到转换后的数据

val result=func.invoke(item)

//将转换后的数据传递给下游

downStream.onNext(result)

}

override fun onError(e: Throwable) {

//将错误传递给下游

downStream.onError(e)

}

override fun onComplete() {

//完成事件传递给下游

downStream.onComplete()

}

}

}

我们定义了一个只属于map自己的观察者对象MlxMapObserver,并且在它的构造方法中,将真正的下游传给了它,在map的观察者对象中,它的所有方法将会传递给真正的下游downStream。

需要特别注意的是在onNext方法中,map方法就是在这一步应用了func的转换,将T类型的数据转换为了R类型,并将R类型的数据传递给了真正的下游downStream。

接下来的事情就很简单了,就是在setObserver方法中去承接上游,将自己的观察者对象给上游

override fun subscribe(downStream: MlxObserver){

val map=MlxMapObserver(downStream,func)//创建自己的观察者对象

source.subscribe(map)//将自己传递给上游

}

如此map就创建完了。是不是很简单呢?

其实代码很简单,但是逻辑可能有点绕,总的来说,就是创建自己的观察者对象,然后将自己的观察者对象给上游,上游传消息给下游其实是传给了map,map在自己的观察者中在对数据进行进一步的操作之后,将操作之后的数据传递给真正的下游。

map的东西创建完了,我们继续回到map方法中,map方法中已知需要返回虚假的观察者对象,而虚假的观察者对象需要一个真正的观察者对象。map就是这个真正的观察者对象,所以我们直接new一个新的虚假的观察者,并且把上游和应用规则全部传递给map,最后将map传递给这个新的虚假的被观察者。

fun map(func:(T)->R):MlxObservable{

//source就是上游真正的被观察者。

val map=MlxMapObservable(this.source!!,func)

return MlxObservable(map)

}

空口无凭,我们来实践一下,看是不是RxJava中的效果。

MlxObservable.create(object :MlxObservableOnSubscribe{

override fun setObserver(emitter: MlxObserver) {

println(“上游发送数据:10”)

emitter.onNext(10)

}

})

.map{ item->

“这是map操作符转换后的数据:$item”

}

.setObserver(object :MlxObserver{

override fun onSubscribe() {}

override fun onNext(item: String) {

println(“下游接收到数据:$item”)

}

override fun onError(e: Throwable) {}

override fun onComplete() {}

})

这是结果:

怎么样,是不是一模一样啊。

可能有小伙伴没看懂,没关系,我们再看一次map的模型示意图。

也就是说,map方法之后返回了一个新的虚假的被观察者对象,这个新的虚假的被观察者对象包含的真正的被观察者是map所构造的被观察者。

也就是说map操作符构造的对象里面,既有被观察者,也有观察者,它的被观察者用于接收下游,它的观察者用于观察上游。

这样是不是很清晰易懂了呢?map操作符的原理就是这么简单,相信小伙伴可以根据map操作符自己实现其他的操作符啦~我就不再班门弄斧了。

接下来我们开始研究RxJava所谓最高深,最难的部分。也就是RxJava是如何切换线程的。

RxJava切换线程


首先RxJava切换线程是使用了两个方法,分别指定上游的线程和下游的线程。

切换上游线程具体是切换了哪个方法的线程呢?用过的小伙伴应该知道,切换的是我们构造的真正的被观察者中所实现的方法。那么这个方法是在哪里被调用的?

没错,是虚假的被观察者调用的。也就是MlxObservable的setObserver方法中调用了上游的setObserver方法。

我们现在如果要改变上游的setObserver方法所在线程,我们只能在虚假的被观察者对象中去改变它。而既然最开始创建的那个虚假的被观察者对象的方法已经写死了,所以我们可以按照map操作符的思想,我们自己去构造一个虚假的被观察者对象,在里面就像map一样承接上游,改变线程。

什么?听不明白?

map是不是承接了上游?

是的。

map怎么承接的上游?

map自己去构造了一个真正的被观察者,然后调用了上游的setObserver方法,把自己设置进去了。

你看,你也知道,map里面调用了上游的setObserver方法把。我们之前讨论了,要改变的不就是这个方法所在线程么,所以我们再定义一个类似map的操作符,然后在别的线程承接上游不就完事了么?

OK,那我们看代码如何实现把。

我们定义一个改变上游线程的操作符,既然是模仿RxJava,那我们就模仿它的方法名subscribeOn把。

改变上游线程

仿照map定义一个类,去实现真正的被观察者接口,同时为了正常的传递数据给下游,也得定义一个自己的观察者对象。

class MlxSubscribeObservable (

val source:MlxObservableOnSubscribe):MlxObservableOnSubscribe{

override fun setObserver(downStream: MlxObserver){

val observer=MlxSubscribeObserver(downStream)

}

class MlxSubscribeObserver(val downStream:MlxObserver):MlxObserver{

}

}

再仿照map定义一个成员方法:

fun subscribeOn():MlxObservable{

val subscribe=MlxSubscribeObservable(this.source!!)

return MlxObservable(subscribe)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2020-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节

还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

一线互联网面试专题

379页的Android进阶知识大全

379页的Android进阶知识大全

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

(this.source!!)

return MlxObservable(subscribe)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-uCGUvKrb-1712423755778)]

[外链图片转存中…(img-NgIlHb1V-1712423755779)]

[外链图片转存中…(img-c1etwsPp-1712423755779)]

[外链图片转存中…(img-iNuQizfg-1712423755780)]

[外链图片转存中…(img-YSofTVXr-1712423755780)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2020-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节

还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

[外链图片转存中…(img-FzX4Yq4t-1712423755780)]

[外链图片转存中…(img-4P2IK8Di-1712423755781)]

[外链图片转存中…(img-p2BlWwai-1712423755781)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值