最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2020-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。
还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
VoidCallback get onMetricsChanged => _onMetricsChanged;
VoidCallback _onMetricsChanged;
Zone _onMetricsChangedZone;
set onMetricsChanged(VoidCallback callback) {
_onMetricsChanged = callback;
_onMetricsChangedZone = Zone.current;
}
//当系统语言发生变化时触发回调
Locale get locale {
if (_locales != null && _locales.isNotEmpty) {
return _locales.first;
}
return null;
}
//获取系统语言
List get locales => _locales;
List _locales;
//当Local发生改变时触发回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
VoidCallback _onLocaleChanged;
Zone _onLocaleChangedZone;
set onLocaleChanged(VoidCallback callback) {
_onLocaleChanged = callback;
_onLocaleChangedZone = Zone.current;
}
//当系统字体大小改变时触发回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
VoidCallback _onTextScaleFactorChanged;
Zone _onTextScaleFactorChangedZone;
set onTextScaleFactorChanged(VoidCallback callback) {
_onTextScaleFactorChanged = callback;
_onTextScaleFactorChangedZone = Zone.current;
}
//屏幕亮度改变时触发回调
VoidCallback get onPlatformBrightnessChanged => _onPlatformBrightnessChanged;
VoidCallback _onPlatformBrightnessChanged;
Zone _onPlatformBrightnessChangedZone;
set onPlatformBrightnessChanged(VoidCallback callback) {
_onPlatformBrightnessChanged = callback;
_onPlatformBrightnessChangedZone = Zone.current;
}
//屏幕刷新时会回调
FrameCallback get onBeginFrame => _onBeginFrame;
FrameCallback _onBeginFrame;
Zone _onBeginFrameZone;
set onBeginFrame(FrameCallback callback) {
_onBeginFrame = callback;
_onBeginFrameZone = Zone.current;
}
//绘制屏幕时回调
VoidCallback get onDrawFrame => _onDrawFrame;
VoidCallback _onDrawFrame;
Zone _onDrawFrameZone;
set onDrawFrame(VoidCallback callback) {
_onDrawFrame = callback;
_onDrawFrameZone = Zone.current;
}
//点击或者指针事件触发回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
PointerDataPacketCallback _onPointerDataPacket;
Zone _onPointerDataPacketZone;
set onPointerDataPacket(PointerDataPacketCallback callback) {
_onPointerDataPacket = callback;
_onPointerDataPacketZone = Zone.current;
}
//获取请求的默认路由
String get defaultRouteName => _defaultRouteName();
String _defaultRouteName() native ‘Window_defaultRouteName’;
//该方法被调用后,onBeginFrame和onDrawFrame将紧连着会在合适时机调用
void scheduleFrame() native ‘Window_scheduleFrame’;
//更新应用在GPU上的渲染,这方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native ‘Window_render’;
//窗口的语义内容是否改变
bool get semanticsEnabled => _semanticsEnabled;
bool _semanticsEnabled = false;
//当窗口语言发生改变时回调
VoidCallback get onSemanticsEnabledChanged => _onSemanticsEnabledChanged;
VoidCallback _onSemanticsEnabledChanged;
Zone _onSemanticsEnabledChangedZone;
set onSemanticsEnabledChanged(VoidCallback callback) {
_onSemanticsEnabledChanged = callback;
_onSemanticsEnabledChangedZone = Zone.current;
}
//当用户表达写的动作时回调
SemanticsActionCallback get onSemanticsAction => _onSemanticsAction;
SemanticsActionCallback _onSemanticsAction;
Zone _onSemanticsActionZone;
set onSemanticsAction(SemanticsActionCallback callback) {
_onSemanticsAction = callback;
_onSemanticsActionZone = Zone.current;
}
//启用其他辅助功能回调
VoidCallback get onAccessibilityFeaturesChanged => _onAccessibilityFeaturesChanged;
VoidCallback _onAccessibilityFeaturesChanged;
Zone _onAccessibilityFlagsChangedZone;
set onAccessibilityFeaturesChanged(VoidCallback callback) {
_onAccessibilityFeaturesChanged = callback;
_onAccessibilityFlagsChangedZone = Zone.current;
}
//更新此窗口的语义数据
void updateSemantics(SemanticsUpdate update) native ‘Window_updateSemantics’;
//设置Isolate调试名称
void setIsolateDebugName(String name) native ‘Window_setIsolateDebugName’;
//向特定平台发送消息
void sendPlatformMessage(String name,
ByteData data,
PlatformMessageResponseCallback callback) {
final String error =
_sendPlatformMessage(name, _zonedPlatformMessageResponseCallback(callback), data);
if (error != null)
throw new Exception(error);
}
String _sendPlatformMessage(String name,
PlatformMessageResponseCallback callback,
ByteData data) native ‘Window_sendPlatformMessage’;
//获取平台消息的回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
PlatformMessageCallback _onPlatformMessage;
Zone _onPlatformMessageZone;
set onPlatformMessage(PlatformMessageCallback callback) {
_onPlatformMessage = callback;
_onPlatformMessageZone = Zone.current;
}
//由_dispatchPlatformMessage调用
void _respondToPlatformMessage(int responseId, ByteData data)
native ‘Window_respondToPlatformMessage’;
//平台信息响应回调
static PlatformMessageResponseCallback _zonedPlatformMessageResponseCallback(PlatformMessageResponseCallback callback) {
if (callback == null)
return null;
// Store the zone in which the callback is being registered.
final Zone registrationZone = Zone.current;
return (ByteData data) {
registrationZone.runUnaryGuarded(callback, data);
};
}
}
可以知道window
包含当前设备系统的一些信息和回调,那么现在看看混入的各种Binding
:
- GestureBinding:绑定手势子系统,提供
onPointerDataPacket
回调。 - ServicesBinding:绑定平台消息通道,提供
onPlatformMessage
回调。 - SchedulerBinding:绑定绘制调度子系统,提供
onBeginFrame
和onDrawFrame
回调。 - PaintingBinding:绑定绘制库,处理图像缓存。
- SemanticsBinding:语义层和flutter engine的桥梁,对辅助功能的底层支持。
- RendererBinding:是渲染树和Flutter engine的桥梁,提供
onMetricsChanged
和onTextScaleFactorChanged
回调。 - WidgetsBinding:是Flutter widget和engine的桥梁,提供
onLocaleChanged
,onBuildSchedule
回调。
也就是WidgetsFlutterBinding.ensureInitialized()
这行代码看名字是将WidgetsFlutterBinding
实例初始化。其实并非那么简单,另外获取一些系统基本信息和初始化监听window
对象的一些事件,然后将这些事件按照上层Framework模型规则进行包装、抽象最后分发。简而言之WidgetsFlutterBinding
是Flutter engine
和Framework
的桥梁。
2.attachRootWidget(app)
第二行是attachRootWidget(app)
,看名字意思是将根RootWidget
挂载。点进去看:
//如果这个widget有必要创建,就将它附加到[renderViewElement]
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter(
container: renderView,
debugShortDescription: ‘[root]’,
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement);
}
renderView
是UI渲染树的根节点:
// The render tree that’s attached to the output surface.
//挂载在渲染树 rootNode根节点
RenderView get renderView => _pipelineOwner.rootNode;
rootWidget
是外面所传进来的根Widget
,这里不用管它,下面看attachToRenderTree(buildOwner, renderViewElement)
这个方法,根据意思,这个方法应该构建RenderTree
,这个方法需要两个参数,首先看buildOwner
这个参数:
/// The [BuildOwner] in charge of executing the build pipeline for the
/// widget tree rooted at this binding.
BuildOwner get buildOwner => _buildOwner;
final BuildOwner _buildOwner = BuildOwner();
看官网对它的定义:
意思是:是widget framework的管理类,用来跟踪哪些widget需要重建,并处理widget树的其他任务,例如管理树的非活动元素列表,并在调试时在热重载期间在必要时触发“重组”命令,下面看另外一个参数renderViewElement
,代码注释如下:
/// The [Element] that is at the root of the hierarchy (and which wraps the
/// [RenderView] object at the root of the rendering hierarchy).
///
/// This is initialized the first time [runApp] is called.
Element get renderViewElement => _renderViewElement;
Element _renderViewElement;
renderViewElement
是renderView
对应的Element
对象,因为renderView
是树的根,所以renderViewElement
位于层次结构的根部,那下面点击attachToRenderTree
的源码看:
/// Inflate this widget and actually set the resulting [RenderObject] as the
/// child of [container].
///
/// If element
is null, this function will create a new element. Otherwise,
/// the given element will have an update scheduled to switch to this widget.
///
/// Used by [runApp] to bootstrap applications.
RenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
跟着代码往下走如果根element
没有创建,那么就调用createElement
创建根element
,element
和widget
进行关联。接着调用element.assignOwner(owner)
,这个方法其实就是设置这个element
的跟踪,最后调用owner.buildScope
这个方法,这个方法是确定更新widget
的范围。如果element
已经创建了,将根element
和关联的widget
设为新的,并且重新构建这个element
,为了后面的复用。
3.scheduleWarmUpFrame
runApp()方法最后一行执行scheduleWarmUpFrame
方法:
/// Schedule a frame to run as soon as possible, rather than waiting for
/// the engine to request a frame in response to a system “Vsync” signal.
///
/// This is used during application startup so that the first frame (which is
/// likely to be quite expensive) gets a few extra milliseconds to run.
///
/// Locks events dispatching until the scheduled frame has completed.
///
/// If a frame has already been scheduled with [scheduleFrame] or
/// [scheduleForcedFrame], this call may delay that frame.
///
/// If any scheduled frame has already begun or if another
/// [scheduleWarmUpFrame] was already called, this call will be ignored.
///
/// Prefer [scheduleFrame] to update the display in normal operation.
void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
return;
_warmUpFrame = true;
Timeline.startSync(‘Warm-up frame’);
final bool hadScheduledFrame = _hasScheduledFrame;
// We use timers here to ensure that microtasks flush in between.
Timer.run(() {
assert(_warmUpFrame);
handleBeginFrame(null);—>1
});
Timer.run(() {
assert(_warmUpFrame);
handleDrawFrame(); ---->2
// We call resetEpoch after this frame so that, in the hot reload case,
// the very next frame pretends to have occurred immediately after this
// warm-up frame. The warm-up frame’s timestamp will typically be far in
// the past (the time of the last real frame), so if we didn’t reset the
// epoch we would see a sudden jump from the old time in the warm-up frame
// to the new time in the “real” frame. The biggest problem with this is
// that implicit animations end up being triggered at the old time and
// then skipping every frame and finishing in the new time.
resetEpoch();
_warmUpFrame = false;
if (hadScheduledFrame)
scheduleFrame();
});
// Lock events so touch events etc don’t insert themselves until the
// scheduled frame has finished.
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
}
首先这个方法在SchedulerBinding
里,这两个方法主要执行了handleBeginFrame
和handleDrawFrame
方法:
3.1.handleBeginFrame
void handleBeginFrame(Duration rawTimeStamp) {
…
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync(‘Animate’, arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
可以看到主要是对transientCallbacks
队列操作,这个集合主要是放一些临时回调,存放动画回调。
3.2.handleDrawFrame
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the “Animate” phase
try {
// PERSISTENT FRAME CALLBACKS ----->
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS ------>
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List localPostFrameCallbacks =
List.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
profile(() {
_profileFrameStopwatch.stop();
_profileFramePostEvent();
});
assert(() {
if (debugPrintEndFrameBanner)
debugPrint(‘▀’ * _debugBanner.length);
_debugBanner = null;
return true;
}());
_currentFrameTimeStamp = null;
}
}
可以看到执行persistentCallbacks
队列,这个队列用于存放一些持久的回调,不能再此类回调中在请求新的绘制帧,持久回调一经注册则不能移除。接着执行postFrameCallbacks
这个队列在每一Frame
(一次绘制)结束时只会调用一次,调用后被系统移除。
也就是scheduleWarmUpFrame
这个方法安排帧尽快执行,当一次帧绘制结束之前不会响应各种事件,这样保证绘制过程中不触发重绘。上面说过:
RendererBinding:是渲染树和Flutter engine的桥梁,提供onMetricsChanged和onTextScaleFactorChanged回调
Flutter真正渲染和绘制是在这个绑定里:
4.渲染绘制
void initInstances() {
super.initInstances();
_instance = this;//初始化
_pipelineOwner = PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
//添加设置监听
window
…onMetricsChanged = handleMetricsChanged
…onTextScaleFactorChanged = handleTextScaleFactorChanged
…onPlatformBrightnessChanged = handlePlatformBrightnessChanged
…onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
…onSemanticsAction = _handleSemanticsAction;
initRenderView();
_handleSemanticsEnabledChanged();
assert(renderView != null);
//添加persistentFrameCallback
addPersistentFrameCallback(_handlePersistentFrameCallback);
//创建触摸管理
_mouseTracker = _createMouseTracker();
}
addPersistentFrameCallback
这个方法主要向persistentFrameCallback添加了回调:
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
再看_handlePersistentFrameCallback
这个回调做了什么:
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();//更新布局信息
pipelineOwner.flushCompositingBits();//在flushLayout只后调用,在flushPaint之前调用,更新RenderObject是否需要重绘
pipelineOwner.flushPaint();//更新绘制RenderObject
renderView.compositeFrame(); // 发送bit数据给GPU
pipelineOwner.flushSemantics(); // 发送语义数据给操作系统
}
下面一个一个方法走:
4.1.flushLayout
void flushLayout() {
profile(() {
Timeline.startSync(‘Layout’, arguments: timelineWhitelistArguments);
});
assert(() {
_debugDoingLayout = true;
return true;
}());
try {
// TODO(ianh): assert that we’re not allowing previously dirty nodes to redirty themselves
while (_nodesNeedingLayout.isNotEmpty) {
final List dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = [];
for (RenderObject node in dirtyNodes…sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {
assert(() {
_debugDoingLayout = false;
return true;
}());
profile(() {
Timeline.finishSync();
});
}
}
看源码得知首先获取哪些标记为脏
的RenderObject
的布局信息,然后通过ode._layoutWithoutResize();
重新调整这些RenderObject
。
4.2.flushCompositingBits
void flushCompositingBits() {
profile(() { Timeline.startSync(‘Compositing bits’); });
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
profile(() { Timeline.finishSync(); });
}
检查RenderObject
是否需要重绘,并且通过node._updateCompositingBits();
更新_needsCompositing
这个属性,若为true
就要重新绘制,否则不需要。
4.3.flushPaint
void flushPaint() {
profile(() { Timeline.startSync(‘Paint’, arguments: timelineWhitelistArguments); });
assert(() {
_debugDoingPaint = true;
return true;
}());
try {
final List dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = [];
// Sort the dirty nodes in reverse order (deepest first).
//方向遍历这些标记过的node
for (RenderObject node in dirtyNodes…sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
//重新绘制
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
assert(_nodesNeedingPaint.isEmpty);
} finally {
assert(() {
_debugDoingPaint = false;
return true;
}());
profile(() { Timeline.finishSync(); });
}
}
这个方法通过反向遍历(dirty
标记)取得需要重绘的RenderObject
,最后通过PaintingContext.repaintCompositedChild(node);
重绘。
4.4.compositeFrame
void compositeFrame() {
Timeline.startSync(‘Compositing’, arguments: timelineWhitelistArguments);
try {
//创建Scene对象
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
//使用render方法将Scene对象显示在屏幕上
_window.render(scene);//调用flutter engine的渲染API
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
return true;
}());
} finally {
Timeline.finishSync();
}
}
Scene
是用来保存渲染后的最终像素信息,这个方法将Canvas
画好的Scene
对象传给window.render()
方法,该方法会直接将Scene
信息发送给Flutter engine,最终Flutter engine将图像画在设备屏幕上,这样整个绘制流程就算完了。
注意:RendererBinding
只是混入对象,最终混入到WidgetsBinding
,回到最开始来看:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
/// Returns an instance of the [WidgetsBinding], creating and
/// initializing it if necessary. If one is created, it will be a
/// [WidgetsFlutterBinding]. If one was previously initialized, then
/// it will at least implement [WidgetsBinding].
///
/// You only need to call this method if you need the binding to be
/// initialized before calling [runApp].
///
/// In the flutter_test
framework, [testWidgets] initializes the
/// binding instance to a [TestWidgetsFlutterBinding], not a
/// [WidgetsFlutterBinding].
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
所以应该WidgetsBinding
来重写实现drawFrame
方法:
@override
void drawFrame() {
assert(!debugBuildingDirtyElements);
assert(() {
debugBuildingDirtyElements = true;
return true;
}());
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame(); //调用Renderbinding的drawFrame方法
buildOwner.finalizeTree();
} finally {
assert(() {
debugBuildingDirtyElements = false;
return true;
}());
}
profile(() {
if (_needToReportFirstFrame && _reportFirstFrame) {
developer.Timeline.instantSync(‘Widgets completed first useful frame’);
developer.postEvent(‘Flutter.FirstFrame’, <String, dynamic>{});
_needToReportFirstFrame = false;
}
});
}
最后
跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ent(‘Flutter.FirstFrame’, <String, dynamic>{});
_needToReportFirstFrame = false;
}
});
}
最后
跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-PAjzcapZ-1715887353734)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!