最后
感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?
Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。
以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。
最后,赠与大家一句诗,共勉!
不驰于空想,不骛于虚声。不忘初心,方得始终。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
public static Observable create(ObservableOnSubscribe source) {
ObjectHelper.requireNonNull(source, “source is null”);
return RxJavaPlugins.onAssembly(new ObservableCreate(source));
}
使用RxJava
可以通过Observable
的create
方法创建一个被观察者对象。create
方法从参数中传入一个ObservableOnSubscribe
类型的source
,然后方法中先校验了source
是否为空,接着将传入的source
封装成一个ObservableCreate
对象,然后调用了RxJavaPlugins.onAssembly
方法返回创建的好的Observable
。接着进入onAssembly
方法查看。
public static Observable onAssembly(@NonNull Observable source) {
Function<? super Observable, ? extends Observable> f = onObservableAssembly;
if (f != null) {
return apply(f, source);
}
return source;
}
onAssembly
方法中首先是一个Hook
实现,这里可以理解为一个代理。可以看到这里先判断onObservableAssembly
是否为空,为空就直接返回传入的source
,否则再调用apply
方法。这里可以继续跟踪一下onObservableAssembly
。
@SuppressWarnings(“rawtypes”)
@Nullable
static volatile Function<? super Observable, ? extends Observable> onObservableAssembly;
/**
-
Sets the specific hook function.
-
@param onObservableAssembly the hook function to set, null allowed
*/
@SuppressWarnings(“rawtypes”)
public static void setOnObservableAssembly(@Nullable Function<? super Observable, ? extends Observable> onObservableAssembly) {
if (lockdown) {
throw new IllegalStateException(“Plugins can’t be changed anymore”);
}
RxJavaPlugins.onObservableAssembly = onObservableAssembly;
}
它是RxJavaPlugins
中的成员变量,默认为空,并且提供了一个set
方法来设置它。因为默认为空,所以默认返回的就是传入的source
。这里的代理默认是不会对Observable
做什么操作,如果需要有特殊的需求可以调用set
方法实现自己的代理。而默认返回的source
类型为ObservableCreate
对象也实现了Observable
接口。
public final class ObservableCreate extends Observable {
final ObservableOnSubscribe source;
public ObservableCreate(ObservableOnSubscribe source) {
this.source = source;
}
…
}
3.2 创建观察者
public interface Observer {
void onSubscribe(@NonNull Disposable d);
void onNext(@NonNull T t);
void onError(@NonNull Throwable e);
void onComplete();
}
观察者Observer
是一个接口,其中提供了一些方法,使用时创建接口的实现,并根据需求在方法中做自己的实现。
3.3 建立订阅关系
建立订阅关系调用了Observable
的subscribe
方法。
public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, “observer is null”);
try {
observer = RxJavaPlugins.onSubscribe(this, observer);
ObjectHelper.requireNonNull(observer, “The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins”);
subscribeActual(observer);
} catch (NullPointerException e) { // NOPMD
…
} catch (Throwable e) {
…
}
}
方法中还是先判断了传入参数observer
是否为空,接着还是一个Hook
实现,这里就不细究了,获得Hook
返回的observer
后再次判断是否为空,之后调用了subscribeActual
方法。
protected abstract void subscribeActual(Observer<? super T> observer);
Observable
的subscribeActual
方法是个抽象方法,之前看过这里的Observable
实际实现是个ObservableCreate
对象,所以再进入ObservableCreate
类查看对应方法。
@Override
protected void subscribeActual(Observer<? super T> observer) {
CreateEmitter parent = new CreateEmitter(observer);
observer.onSubscribe(parent);
try {
source.subscribe(parent);
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
parent.onError(ex);
}
}
ObservableCreate
中的subscribeActual
方法中先创建了一个CreateEmitter
发射器对象,并将observer
对象传入。接着调用了observer
的onSubscribe
方法,此时观察者的onSubscribe
方法执行。最后调用了source
的subscribe
方法。
Observable observable = Observable.create(new ObservableOnSubscribe() {
@Override
public void subscribe(ObservableEmitter emitter) throws Exception {
Log.d(getClass().getName(), Thread.currentThread().getName() + " ObservableOnSubscribe subscribe");
emitter.onNext(“string1”);
emitter.onNext(“string2”);
emitter.onNext(“string3”);
emitter.onComplete();
}
});
这个source
就是在create
方法中传入的ObservableOnSubscribe
。它的subscribe
方法中通过调用ObservableEmitter
的方法发送事件,这里的ObservableEmitter
就是之前创建的CreateEmitter
对象,所以再来进一步看看它其中的方法。
CreateEmitter(Observer<? super T> observer) {
this.observer = observer;
}
@Override
public void onNext(T t) {
if (t == null) {
onError(new NullPointerException(“onNext called with null. Null values are generally not allowed in 2.x operators and sources.”));
return;
}
if (!isDisposed()) {
observer.onNext(t);
}
}
@Override
public void onComplete() {
if (!isDisposed()) {
try {
observer.onComplete();
} finally {
dispose();
}
}
}
CreateEmitter
的构造函数接收了观察者对象,然后在调用onNext
方法时先做了空判断,再对isDisposed
进行取消订阅的判断,之后调用了observer
的onNext
方法,也就是观察者的onNext
方法。同样的onComplete
中最终也是调用了observer
的onComplete
方法。至此RxJava
中的基本订阅流程的源码就梳理完了。
4. 线程切换
RxJava
中有个很重要的功能,就是能方便的切换线程,来看下它的使用,还是之前基础使用中的例子进行修改。
Observable observable0 = Observable.create(new ObservableOnSubscribe() {
@Override
public void subscribe(ObservableEmitter emitter) throws Exception {
Log.d(getClass().getName(), Thread.currentThread().getName() + " ObservableOnSubscribe subscribe");
emitter.onNext(“string1”);
emitter.onNext(“string2”);
emitter.onNext(“string3”);
emitter.onComplete();
}
});
Observer observer = new Observer() {
@Override
public void onSubscribe(Disposable d) {
Log.d(getClass().getName(), Thread.currentThread().getName() + " onSubscribe");
}
@Override
public void onNext(String s) {
Log.d(getClass().getName(), Thread.currentThread().getName() + " onNext "+s);
}
@Override
public void onError(Throwable e) {
Log.d(getClass().getName(), Thread.currentThread().getName() + " onError");
}
@Override
public void onComplete() {
Log.d(getClass().getName(), Thread.currentThread().getName() + " onComplete");
}
};
Observable observable1 = observable0.subscribeOn(Schedulers.newThread());
Log.d(getClass().getName(), Thread.currentThread().getName() + " observable1:"+observable1.getClass().getName());
Observable observable2 = observable1.observeOn(AndroidSchedulers.mainThread());
Log.d(getClass().getName(), Thread.currentThread().getName() + " observable2:"+observable2.getClass().getName());
observable2.subscribe(observer);
被观察者和观察者的创建和之前一样,在建立订阅关系时调用subscribeOn
和observeOn
方法进行线程的切换。这里每个方法返回的都是Observable
类型,所以可以采用链式调用,这也是RxJava
的一个特点,但是这里没有采用这种写法,而是将其拆分开来写并且日志打印出每个Observable
的具体类型,这是为了方便之后源码理解。 运行结果日志:
4.1 subscribeOn
Observable observable1 = observable0.subscribeOn(Schedulers.newThread());
Log.d(getClass().getName(), Thread.currentThread().getName() + " observable1:"+observable1.getClass().getName());
observable1.subscribe(observer);
运行结果:
先只调用subscribeOn
方法运行查看结果,发现不仅被观察者发射事件运行在了子线程,观察者接收事件也运行在子线程,那么进入subscribeOn
方法查看它的实现。
public final Observable subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, “scheduler is null”);
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn(this, scheduler));
}
可以看到subscribeOn
方法和subscribe
方法有些类似。首先是判断传入的scheduler
是否为空,然后同样调用RxJavaPlugins.onAssembly
方法,这次构建了一个ObservableSubscribeOn
对象返回。而subscribeOn
方法之后还是调用了subscribe
方法,根据之前的分析,subscribe
方法最终会调用到subscribeActual
方法,不过此时的subscribeActual
方法不再是ObservableCreate
中的而是ObservableSubscribeOn
中的subscribeActual
方法。
@Override
public void subscribeActual(final Observer<? super T> observer) {
final SubscribeOnObserver parent = new SubscribeOnObserver(observer);
observer.onSubscribe(parent);
parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
}
ObservableSubscribeOn
的subscribeActual
方法中流程和之前的也很类似,这次是先创建了一个SubscribeOnObserver
对象,将观察者对象传入,接着同样先调用了observer.onSubscribe
方法,然后将传入的SubscribeOnObserver
封装入了一个SubscribeTask
对象中,接着调用了scheduler.scheduleDirect
方法再将返回结果得到的Disposable
设置到SubscribeOnObserver
中。下面一个方法一个方法看。首先是创建SubscribeTask
。
final class SubscribeTask implements Runnable {
private final SubscribeOnObserver parent;
SubscribeTask(SubscribeOnObserver parent) {
this.parent = parent;
}
@Override
public void run() {
source.subscribe(parent);
}
}
SubscribeTask
是ObservableSubscribeOn
的内部类,其实现很简单就是实现了一个Runnable
接口,构造方法中传入了SubscribeOnObserver
对象,在其run
方法中调用了ObservableSubscribeOn
中的成员变量source
的subscribe
方法。这个source
是在创建ObservableSubscribeOn
时传入的,根据前面的代码可以找到是在subscribeOn
方法中创建的对象并且这个source
对应传入的是当前这个Observable
对象即通过Observable.create
获得的被观察者对象,其实现之前看过是一个ObservableCreate
所以这里就和之前一样又会走到了其父类Observable
的subscribe
方法中,继而调用ObservableCreate
的subscribeActual
方法,之后最终会调用到观察者的对应onNext
等方法,不过此时的观察者不直接是在使用时创建传入的Observer
,而是之前看到的SubscribeOnObserver
类型,不过其中的onNext
等方法还是调用了在使用时创建传入的Observer
的对应方法。
static final class SubscribeOnObserver extends AtomicReference implements Observer, Disposable {
private static final long serialVersionUID = 8094547886072529208L;
final Observer<? super T> downstream;
final AtomicReference upstream;
SubscribeOnObserver(Observer<? super T> downstream) {
this.downstream = downstream;
this.upstream = new AtomicReference();
}
@Override
public void onNext(T t) {
downstream.onNext(t);
}
@Override
public void onError(Throwable t) {
downstream.onError(t);
}
@Override
public void onComplete() {
downstream.onComplete();
}
…
}
下面接着看到scheduleDirect
这个方法,在创建好SubscribeTask
之后调用了scheduleDirect
方法。这里的scheduler
就是subscribeOn
中传入的,对应开始例子中的Schedulers.newThread
。
public static Scheduler newThread() {
return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD);
}
// 静态成员变量NEW_THREAD
static final Scheduler NEW_THREAD;
NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());
进入Schedulers.newThread
一步步跟踪,看到newThread
方法返回静态成员变量中的NEW_THREAD
,而NEW_THREAD
又是通过NewThreadTask
创建。
static final class NewThreadTask implements Callable {
@Override
public Scheduler call() throws Exception {
return NewThreadHolder.DEFAULT;
}
}
static final Scheduler DEFAULT = new NewThreadScheduler();
继续跟踪查看发现NewThreadTask
实际是实现了Callable
接口,其call
方法中返回了静态内部类中的NewThreadHolder.DEFAULT
。这个DEFAULT
的实现类型为NewThreadScheduler
。至此终于找到了我们传入的Scheduler
的真正实现类。于是继续看其scheduleDirect
方法。
public Disposable scheduleDirect(@NonNull Runnable run) {
return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS);
}
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
final Worker w = createWorker();
final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
DisposeTask task = new DisposeTask(decoratedRun, w);
w.schedule(task, delay, unit);
return task;
}
scheduleDirect
方法是在其父类中实现的,看到其中进而调用了同名重载方法,方法中首先是调用createWorker
方法创建一个Worker
。这个方法的实现就是在NewThreadScheduler
中了。
public Worker createWorker() {
return new NewThreadWorker(threadFactory);
}
createWorker
方法中只做了一件事就是创建返回了一个NewThreadWorker
。
public class NewThreadWorker extends Scheduler.Worker implements Disposable {
private final ScheduledExecutorService executor;
volatile boolean disposed;
public NewThreadWorker(ThreadFactory threadFactory) {
executor = SchedulerPoolFactory.create(threadFactory);
}
…
}
NewThreadWorker
中看到创建了一个线程池,再回到scheduleDirect
方法,创建完Worker
后将传入的Runnable
即SubscribeTask
进行一个装饰得到新的Runnable
对象。接着将Worker
和新的Runnable
封装到一个DisposeTask
对象中。
static final class DisposeTask implements Disposable, Runnable, SchedulerRunnableIntrospection {
@NonNull
final Runnable decoratedRun;
@NonNull
final Worker w;
@Nullable
Thread runner;
DisposeTask(@NonNull Runnable decoratedRun, @NonNull Worker w) {
this.decoratedRun = decoratedRun;
this.w = w;
}
@Override
public void run() {
runner = Thread.currentThread();
try {
decoratedRun.run();
} finally {
dispose();
runner = null;
}
}
…
}
DisposeTask
同样实现了Runnable
接口,在run
方法中调用了从构造传入的decoratedRun
的run
方法执行任务。回到最后一步,调用Worker
的schedule
方法,这里就对应的NewThreadWorker
的schedule
方法。
public Disposable schedule(@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) {
if (disposed) {
return EmptyDisposable.INSTANCE;
}
return scheduleActual(action, delayTime, unit, null);
}
schedule
方法中又进一步调用了其scheduleActual
方法
public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) {
Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent);
if (parent != null) {
if (!parent.add(sr)) {
return sr;
}
}
Future<?> f;
try {
if (delayTime <= 0) {
f = executor.submit((Callable)sr);
} else {
f = executor.schedule((Callable)sr, delayTime, unit);
}
sr.setFuture(f);
} catch (RejectedExecutionException ex) {
if (parent != null) {
parent.remove(sr);
}
RxJavaPlugins.onError(ex);
}
return sr;
}
scheduleActual
方法里看到又将decoratedRun
和DisposableContainer
封装成ScheduledRunnable
最后将这个ScheduledRunnable
交给构造函数中创建的线程池去运行,最终就会执行到前面看过的SubscribeTask
中的run
方法完成订阅逻辑,调用观察者的onNext
等方法。到这里就看出最终的source.subscribe
是会通过线程池切换到子线程中去执行了。
通过查看subscribeOn
方法源码可以发现,方法里实际上是在前一个创建的ObservableCreate
外面包了一层,把它包成一个ObservableSubscribeOn
对象,同样的原先的Observer
也被包了一层包成一个SubscribeOnObserver
对象,而线程切换的工作是由Scheduler
完成的。
4.2 observeOn
接着再来看看切换回主线程的方法observeOn
,还是先修改使用代码,查看运行日志。
Observable observable2 = observable0.observeOn(AndroidSchedulers.mainThread());
Log.d(getClass().getName(), Thread.currentThread().getName() + " observable2:"+observable2.getClass().getName());
observable2.subscribe(observer);
运行日志:
接着还是进入来看源码。
public final Observable observeOn(Scheduler scheduler) {
return observeOn(scheduler, false, bufferSize());
}
public final Observable observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
ObjectHelper.requireNonNull(scheduler, “scheduler is null”);
ObjectHelper.verifyPositive(bufferSize, “bufferSize”);
return RxJavaPlugins.onAssembly(new ObservableObserveOn(this, scheduler, delayError, bufferSize));
}
这里看到observeOn
方法里调用了重载方法,方法中还是同一个套路,不过这里创建的又是另一个对象ObservableObserveOn
了。根据前面的经验这里就又是将前一个Observable
传递到ObservableObserveOn
中的成员变量source
上,这里看到就是构造函数中的第一个参数。接着还是会调用subscribe
与观察者建立订阅关系进而会执行到ObservableObserveOn
对象的subscribeActual
方法。
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize));
}
}
subscribeActual
方法中判断了scheduler
的类型,这里的scheduler
就是由AndroidSchedulers.mainThread()
传入的,于是先来看一下这个方法。
public static Scheduler mainThread() {
return RxAndroidPlugins.onMainThreadScheduler(MAIN_THREAD);
}
private static final Scheduler MAIN_THREAD = RxAndroidPlugins.initMainThreadScheduler(
new Callable() {
@Override public Scheduler call() throws Exception {
return MainHolder.DEFAULT;
}
});
private static final class MainHolder {
static final Scheduler DEFAULT = new HandlerScheduler(new Handler(Looper.getMainLooper()), false);
}
从mainThread
开始看,发现代码调用逻辑和之前的Schedulers.newThread
方法类似,最终会返回一个HandlerScheduler
而这个Scheduler
中的Handler
则是主线程的Handler
,看到这里就能猜想到了,后面观察者的对应方法一定是由这个Handler
来切换到主线程执行的。回到subscribeActual
方法。
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize));
}
}
这里判断完类型会走else
中的方法首先还是会调用HandlerScheduler
的createWorker
方法创建一个Worker
。
@Override
public Worker createWorker() {
return new HandlerWorker(handler, async);
}
这里是个HandlerWorker
其中具体方法后面再看。接着上面创建完Worker
后同样还是一样调用source.subscribe
创建了一个ObserveOnObserver
对象传入。这里的source
就还是之前的ObservableCreate
,所以这里还是会调用ObservableCreate
中的subscribeActual
方法。
protected void subscribeActual(Observer<? super T> observer) {
CreateEmitter parent = new CreateEmitter(observer);
observer.onSubscribe(parent);
try {
source.subscribe(parent);
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
parent.onError(ex);
}
}
ObservableCreate
中的subscribeActual
方法中的逻辑之前看过,不过此时传入的observer
仍然不再是在使用时创建的观察者对象了,而是传过来的ObserveOnObserver
对象,此时创建的CreateEmitter
中的observer
也就是这个ObserveOnObserver
对象。和之前逻辑一样,接着就会调用observer
的onNext
等方法,此时调用的即是ObserveOnObserver
中的onNext
等方法。所以进入ObserveOnObserver
查看。
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
}
@Override
public void onComplete() {
总结
首先是感觉自己的基础还是不够吧,大厂好像都喜欢问这些底层原理。
另外一部分原因在于资料也还没有看完,一面时凭借那份资料考前突击恶补个几天居然也能轻松应对(在这里还是要感谢那份资料,真的牛),于是自我感觉良好,资料就没有怎么深究下去了。
之前的准备只涉及了Java、Android、计网、数据结构与算法这些方面,面对面试官对其他基础课程的考察显得捉襟见肘。
下一步还是要查漏补缺,进行针对性复习。
最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ObservableCreate
中的subscribeActual
方法中的逻辑之前看过,不过此时传入的observer
仍然不再是在使用时创建的观察者对象了,而是传过来的ObserveOnObserver
对象,此时创建的CreateEmitter
中的observer
也就是这个ObserveOnObserver
对象。和之前逻辑一样,接着就会调用observer
的onNext
等方法,此时调用的即是ObserveOnObserver
中的onNext
等方法。所以进入ObserveOnObserver
查看。
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
}
@Override
public void onComplete() {
总结
首先是感觉自己的基础还是不够吧,大厂好像都喜欢问这些底层原理。
另外一部分原因在于资料也还没有看完,一面时凭借那份资料考前突击恶补个几天居然也能轻松应对(在这里还是要感谢那份资料,真的牛),于是自我感觉良好,资料就没有怎么深究下去了。
之前的准备只涉及了Java、Android、计网、数据结构与算法这些方面,面对面试官对其他基础课程的考察显得捉襟见肘。
下一步还是要查漏补缺,进行针对性复习。
最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!
[外链图片转存中…(img-BMuPHGrK-1715445743040)]
[外链图片转存中…(img-5WI7bM5A-1715445743041)]
[外链图片转存中…(img-jTiWmTt3-1715445743041)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!