将 ViewController 拆分为多个 Category,按 View 构造、布局、更新、业务线逻辑将代码拆分到 Category。这个方式可以解决部分问题,但有限,当功能非常复杂时就无法很好的支撑了,主要问题有:
-
拆分了 ViewController,但是 IPER 层没有拆分,拆分的不彻底,职责还是相互耦合
-
Category 之间互相访问需要的属性、内部方法时,需要暴露在头文件中,而这些是应该隐藏的
-
无法支持批量调用,如 ViewDidLoad 时机,需要各个 Category 方法定义不同方法(同名会被覆盖),逐个调用
左侧和底部的子功能放在一个 UIStackView 中
这个思路方向大体正确了,但是在尝试大半年后失败,删掉了代码。
正确的点在于:抽象了子功能之间的关系,利用 UIStackView 做布局。
失败的点在于:
-
局部重构:仅仅是局部重构,没有深入的分析整体功能、逻辑,没有彻底解决问题,Masonry 布局代码和 UIStackView 使用方式都放在 ViewController 中,不同功能的 view 很容易耦合,劣化依然存在,很快又然难以维护,这类似破窗效应
-
实施方案不完善:布局需要实现 2 套代码,开发、测试同学非常容易忽略,线上经常出问题
-
UIStackView crash:概率性 crash,崩在系统库中,大半年时间也没有找到原因
其他
还有一些提出 MVP、MVVM 等结构的方案,有的浅尝辄止、有的通过不了技术评审、有的不了了之。
关键问题
上面仅列举部分问题,如果按人头收集,那将数不胜数,但这些基本都是表象问题,找到问题的本质、原因,解决关键问题,才能彻底解决问题,很多表象问题也会被顺带解决。
经常提到的内聚、耦合、封装、分层等等思想感觉很好,用时却又没有真正解决问题,下面扩展两点,辅助分析、解决问题:
-
复杂度
-
“变量”与“常量”
复杂度
复杂功能难以维护的原因的是因为复杂。
是的,很直接,相对的,设计、重构等手法都是让事情变得简单,但变简单的过程并不简单,从 2 个角度切入来拆解:
-
量
-
关系
量:量是显性的,功能不断增加,相应的需要更多人来开发、维护,需要写更多代码,也就越来越难维护,这些是显而易见的。
关系:关系是隐性的,功能之间产生耦合即为发生关系,假设 2 个功能之间有依赖,关系数量记为 1,那 3 者之间关系数量为 3,4 者之间关系数量为 6,这是一个指数增加的,当数量足够大时,复杂度会很夸张,关系并不容易看出来,因此很容易产生让人意想不到的变化。
功能的数量大体可以认为是随产品人数线性增长的,即复杂度也是线性增长,随着开发人数同步增长是可以继续维护的。如果关系数量指数级增长,那么很快就无法维护了。
“变量”与“常量”
“变量”是指相比上几个版本,哪些代码变了,与之对应的“常量”即哪些代码没变,目的是:
从过去的变化中找到规律,以适应未来的变化。
平常提到的封装、内聚、解耦等概念,都是静态的,即某一个时间点合理,不意味着未来也合理,期望改进可以在更长的时间范围内合理,称之为动态,找到代码中的“变量”与“常量”是比较有效的手段,相应的代码也有不同的优化趋向:
-
对于“变量”,需要保证职责内聚、单一,易扩展
-
对于“常量”,需要封装,减少干扰,对使用者透明
回到交互区重构场景,发现新加的子功能,基本都加在固定的 3 个区域中,布局是上下撑开,这里的变指的就是新加的子功能,不变指的是加的位置和其他子功能的位置关系、逻辑关系,那么变化的部分,可以提供一个灵活的扩展机制来支持,不变的部分中,业务无关的下沉为底层框架,业务相关的封装为独立模块,这样整体的结构也就出来了。
“变量”与“常量”同样可以检验重构效果,比如模块间常常通过抽象出的协议进行通信,如果通信方法都是具体业务的,那每个同学都可能往里添加各自的方法,这个“变量”就会失去控制,难以维护。
设计方案
梳理问题的过程中,已经在不断的在思考什么样的方式可以解决问题,大致雏形已经有了,这部分更多的是将设计方案系统化。
思路
-
通过上述梳理功能发现 UI 设计和产品的规律:
-
- 整体可分为 3 个区域,左侧、右侧、底部,每个子功能都可以归到 3 个区域中,按需显示,数据驱动
-
左侧区域中的作者名称、描述、音乐信息是自底向上挨个排列
-
右侧主要是按钮类型,头像、点赞、评论,排列方式和左侧规律相同
-
底部可能有个警告、热点,只显示 1 个或者不显示
-
为了统一概念,将 3 个区域定义为容器、容器中放置的子功能定义为元素,容器边界和能力可以放宽一些,支持弱类型实例化,这样就能支持物理隔离元素代码,形成一个可插拔的机制。
-
元素将 View、布局、业务逻辑代码都内聚在一起,元素和交互区、元素和元素之间不直接依赖,职责内聚,便于维护。
-
众多的接口可以抽象归类,大体可分为 UI 生命周期调用、播放器生命周期调用,将业务性的接口抽象,分发到具体的元素中处理逻辑。
架构设计
下图是期望达到的最终目标形态,实施过程会分为多步,确定最终形态,避免实施时偏离目标。
整体指导原则:简单、适用、可演化。
-
SDK 层:抽象出和业务完全无关的 SDK 层,SDK 负责管理 Element、Element 间通信
-
业务框架层:将通用业务、共性代码等低频率修改代码独立出来,形成框架层,这层代码是可由专人维护,业务线同学无法修改
-
业务扩展层:各业务线具体的子功能在此层实现,提供灵活的注册、插拔能力,Element 间无耦合,代码影响限定在 Element 内部
SDK 层
Container
所有的 Element 都通过 Container 来管理,包括 2 部分:
-
对 Element 的创建、持有
-
持有了一个 UIStackView,Element 创建的 View 都加入到此 UIStackView 中
使用 UIStackView 是为了实现自底向上的流式布局。
Element
子功能的 UI、逻辑、操作等所有代码封装的集合体,定义为 Element,借鉴了网页中的 Element 概念,对外的行为可抽象为:
-
View:最终显示的 View,lazy 的形式构造
-
布局:自适应撑开,Container 中的 UIStackView 可以支持
-
事件:通用的事件,处理 handler 即可,view 内部也可自行添加事件
-
更新:传入模型,内部根据模型内容,赋值到 view 中
View
View 在 BaseElement 中的定义如下:
@interface BaseElement : NSObject
@property (nonatomic, strong, nullable) UIView *view;
@property (nonatomic, assign) BOOL appear;
- (void)viewDidLoad;
@end
-
BaseElement 是抽象基类,公开 view 属性形式上看 view 属性、viewDidLoad 方法,和 UIViewController 使用方式的非常类似,设计意图是想靠向 UIViewController,以便让大家更快的接受和理解
-
appear 表示 element 是否显示,appear 为 YES 时,view 被自动创建,viewDidLoad 方法被调用,相关的子 view、布局等业务代码在 viewDidLoad 方法中复写,和 UIViewController 使用类似
-
appear 和 hidden 的区别在于,hidden 只是视觉看不到了,内存并没有释放掉,而低频次使用的 view 没必要常驻内存,因此 appear 为 NO 时,会移除 view 并释放内存
布局
-
UIStackView 的 axis 设置了 UILayoutConstraintAxisVertical,布局时自底向上的流式排列
-
容器内的元素自下向上布局,最底部的元素参照容器底部约束,依次布局,容器高度参照最上面的元素位置
-
元素内部自动撑开,可直接设置固定高度,也可以用 autolayout 撑开
事件
@protocol BaseElementProtocol
@optional
- (void)tapHandler:(UITapGestureRecognizer *)sender;
@end
-
实现协议方法,自动添加手势,支持点击事件
-
也可以自行添加事件,如按钮,使用原生的 addTarget 点击体验更好
更新
data 属性赋值,触发更新,通过 setter 形式实现。
@property (nonatomic, strong, nullable) id data;
赋值时会调用 setData 方法。
- (void)setData:(id)data {
_data = data;
[self processAppear:self.appear];
}
赋值时,processAppear 方法会根据 appear 状态更新 View 的状态,决定创建或销毁 View。
数据流图
Element 的生命周期、更新时的数据流向示意图,这里就不细讲了。
动画特效
图中是实际需要支持的业务场景,目前是 ABTest 阶段,老代码实现方式主要问题:
-
对每处 view 都用 GET_AB_TEST_CASE(videoPlayerInteractionOptimization)判断处理了,代码中共有 32 处判断
-
每个 View 使用 Transform 动画隐藏
这个实现方式非常分散,加新 view 时很容易被遗漏,Element 支持更优的方式:
-
左侧所有子功能都在一个容器中,因此隐藏容器即可,不需要操作每个子功能
-
右侧单独隐藏头像、音乐单独处理即可
扩展性
Element 之间无依赖,可以做到每个 Element 物理隔离,代码放在各自的业务组件中,业务组件依赖交互区业务框架层即可,独立的 Element 通过 runtime 形式,使用注册的方式提供给交互区,框架会将字符串的类实例化,让其正常工作。
[self.container addElementByClassName:@“PlayInteractionAuthorElement”];
[self.container addElementByClassName:@“PlayInteractionRateElement”];
[self.container addElementByClassName:@“PlayInteractionDescriptionElement”];
业务框架层
容器管理
SDK 中仅提供了容器的抽象定义和实现,在业务场景中,需要结合具体业务场景,进一步定义容器的范围和职责。
上面梳理了功能中将整个页面分为左侧、右侧、底部 3 个区域,那么这 3 个区域就是相应的容器,所有子功能都可以归到这 3 个容器中,如下图:
协议
Feed 是用 UITableView 实现,Cell 中除了交互区外只有播放器,因此所有的外部调用都可以抽象,如下图所示。
从概念上讲只需要 1 个交互区协议,但这里可以细分为 2 部分:
-
页面生命周期
-
播放器生命周期
所有 Element 都要实现这个协议,因此在 SDK 中的 Element 基类之上,继承实现了 PlayInteractionBaseElement,这样具体 Element 中不需要实现的方法可以不写。
@interface PlayInteractionBaseElement : BaseElement
@end
为了更清晰定义协议职责,用接口隔离的思想继续拆分,PlayInteractionDispatcherProtocol 作为统一的聚合协议。
@protocol PlayInteractionDispatcherProtocol <PlayInteractionCycleLifeDispatcherProtocol, PlayInteractionPlayerDispatcherProtocol>
@end
页面生命周期协议:PlayInteractionCycleLifeDispatcherProtocol
简单列了部分方法,这些方法都是 ViewController、TableView、Cell 对应的生命周期方法,是完全抽象的、和业务无关的,因此不会随着业务量的增加而膨胀。
@protocol PlayInteractionCycleLifeDispatcherProtocol
- (void)willDisplay;
- (void)setHide:(BOOL)flag;
- (void)reset;
@end
播放器生命周期协议:PlayInteractionPlayerDispatcherProtocol
播放器的状态和方法,也是抽象的、和业务无关。
@protocol PlayInteractionPlayerDispatcherProtocol
@property (nonatomic, assign) PlayInteractionPlayerStatus playerStatus;
- (void)pause;
- (void)resume;
- (void)videoDidActivity;
@end
Manager - 弹窗、蒙层
弹窗、蒙层的 view 规律并不在容器管理之中,所以需要一套额外的管理方式,这里定义了 Manager 概念,是一个相对抽象的概念,即可以实现弹窗、蒙层等功能,也可以实现 View 无关的功能,和 Element 同样,将代码拆分开。
@interface PlayInteractionBaseManager : NSObject
- (UIView *)view;
@end
-
PlayInteractionBaseManager 同样实现了 PlayInteractionDispatcherProtocol 协议,因此具备了所有的交互区协议调用能力
-
Manager 不提供 View 的创建能力,这里的 view 是 UIViewController 的 view 引用,比如需要加蒙层,那么加到 manager 的 view 中就相当于加到 UIViewController 的 view 中
-
弹窗、蒙层通过此种方式实现,Manager 并不负责弹窗、蒙层间的互斥、优先级逻辑处理,需要单独的机制去做
方法派发
业务框架层中定义的协议,需要框架层调用,SDK 层是感知不到的,由于 Element、Manager 众多,需要一个机制来封装批量调用过程,如下图所示:
分层结构
旧交互区使用了 VIPER 范式,抖音里整体使用的 MVVM,多套范式会增加学习、维护成本,并且使用 Element 开发时,VIPER 层级过多,因此考虑统一为 MVVM。
VIPER 整体分层结构
MVVM 整体分层结构
在 MVVM 结构中,Element 职责和 ViewController 概念很接近,也可以理解为更纯粹、更专用的的 ViewController。
经过 Element 拆分后,每个子功能已经内聚在一起,代码量是有限的,可以比较好的支撑业务开发。
Element 结合 MVVM 结构
-
Element:如果是特别简单的元素,那么只提供 Element 的实现即可,Element 层负责基本的实现和跳转
-
ViewModel:部分元素逻辑比较复杂,需要将逻辑抽离出来,作为 ViewModel,对应目前的 Presentor 层
-
Tracker:埋点工具,埋点也可以写在 VM 中,对应目前的 Interactor
-
Model:绝大多数使用主 Model 即可
业务层
业务层中存放的是 Element 实现,主要有两种类型:
-
通用业务:如作者信息、描述、头像、点赞、评论等通用的功能
-
子业务线业务:十几条子业务线,不一一列举
通用业务 Element 和交互区代码放在一起,子业务线 Element 放在业务线中,代码物理隔离后,职责会更明确,但是这也带来一个问题,当框架调整时,需要改多个仓库,并且可能修改遗漏,所以重构初期可以先放一起,稳定后再迁出去。
过度设计误区
设计往往会走两个极端,没有设计、过度设计。
所谓没有设计是在现有的架构、模式下,没有额外思考过差异、特点,照搬使用。
过渡设计往往是在吃了没有设计的亏后,成了惊弓之鸟,看什么都要搞一堆配置、组合、扩展的设计,简单的反而搞复杂了,过犹不及。
设计是在质量、成本、时间等因素之间做出权衡的艺术。
实施方案
业务开发不能停,一边开发、一边重构,相当于在高速公路上不停车换轮胎,需要有足够的预案、备案,才能保证设计方案顺利落地。
改动评估
先估算一下修改规模、周期:
-
代码修改量:近 4 万行
-
时间:半年
改动巨大、时间很长,风险是难以控制的,每个版本都有大量业务需求,需要改大量的代码,在重构的同时,如果重构的代码和新需求代码冲突,是非常难解的,因此考虑分期。
上面已经多次说到功能的重要性,需要考虑重构后,功能是否正常,如果出了问题如何处理、如何证明重构后的功能和之前是一致的,对产品数据无影响。
实施策略
基本思路是实现一个新页面,通过 ABTest 来切换,核心指标无明显负向则放量,全量后删除旧代码,示意图如下:
共分为三期:
-
一期改造内容如上图红色所示:抽取协议,面向协议编程,不依赖具体类,改造旧 VC,实现协议,将协议之外暴露的方法、属性收敛到内部
-
二期改造内容如蓝色所示:新建个新 VC,新 VC 和旧 VC 在功能上是完全一致,实现协议,通过 ABTest 来控制使用方拿到的是旧 VC 还是新 VC
-
三期内容:删掉旧 VC、ABTest,协议、新 VC 保留,完成替换工作
其中二期是重点,占用时间最多,此阶段需要同时维护新旧两套页面,开发、测试工作量翻倍,因此要尽可能的缩短二期时间,不要着急改代码,可以将一期做完善了、各方面的设计准备好再开始。
ABTest
2 个目的:
-
利用 ABTest 作为开关,可以灵活的切换新旧页面
-
用数据证明新旧页面是一致的,从业务功能上来说,二者完全一致,但实际情况是否符合预期,需要用留存、播放、渗透率等核心指标证明
两套页面的开发方式
在二期中,两套页面 ABTest 切换方式是有成本的,需求开发两套、测试两遍,虽然部分代码可共用,但成本还是大大增加,因此需要将这个阶段尽可能缩短。
另外开发、测试两套,不容易发现问题,而一旦出问题,即便能用 ABTest 灵活切换,但修复问题、重新上线、ABTest 数据有结论,也需要非常长的周期。
如果每个版本都出问题,那将会是上线、发现问题,重新修复再上线,又发现了新问题,无限循环,可能一直无法全量。
如上图所示,版本单周迭代,发现问题跟下周修复,那么需要经过灰度、上线灰度(AppStore 的灰度放量)、ABTest 验证(AB 数据稳定要 2 周),总计要 6 周的时间。
让每个同学理解整体运作机制、成本,有助于统一目标,缩短此阶段周期。
删掉旧代码
架构设计上准备充足,删掉旧代码非常简单,删掉旧文件、ABTest 即可,事实上也是如此,1 天内就完成了。
代码后入后,有些长尾的事情会持续 2、3 个版本,例如有些分支,已经修改了删掉的代码,因为文件已经不存在了,只要修改,必定会冲突,合之前,需要 git merge 一下源分支,将有冲突的老页面再删掉。
防崩溃兜底
面向协议开发两套页面,如果增加一个功能时,新页面遗漏了某个方法的话,期望可以不崩溃。利用 Objective-C 语言消息转发可以实现这特性,在 forwardingTargetForSelector 方法中判断方法是否存在,如果不存在,添加一个兜底方法即可,用来处理即可。
- (id)forwardingTargetForSelector:(SEL)aSelector {
Class clazz = NSClassFromString(@“TestObject”);
if (![self isExistSelector:aSelector inClass:clazz]) {
class_addMethod(clazz, aSelector, [self safeImplementation:aSelector], [NSStringFromSelector(aSelector) UTF8String]);
}
Class Protector = [clazz class];
id instance = [[Protector alloc] init];
return instance;
}
- (BOOL)isExistSelector:(SEL)aSelector inClass:(Class)clazz {
BOOL isExist = NO;
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(clazz, &methodCount);
NSString *aSelectorName = NSStringFromSelector(aSelector);
for (int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *selectorName = NSStringFromSelector(selector);
if ([selectorName isEqualToString: aSelectorName]) {
isExist = YES;
break;
}
}
return isExist;
}
- (IMP)safeImplementation:(SEL)aSelector {
IMP imp = imp_implementationWithBlock(^(){
// log
});
return imp;
}
线上兜底降低影响范围,内测提示尽早发现,在开发、内测阶段时可以用比较强的交互手段提示,如 toast、弹窗等,另外可以接打点上报统计。
防劣化
需要明确的规则、机制防劣化,并持续投入精力维护。
不是每个人都能理解设计意图,不同职责的代码放在应该放的位置,比如业务无关的代码,应该下沉到框架层,降低被破坏的概率,紧密的开发节奏,即便简单的 if else 也容易写出问题,例如再加 1 个条件,几乎都会再写 1 个 if,直至写了几十个后,发现写不下去了,再推倒重构,期望重构一次后,可以保持得尽可能久一些。
更严重的是在重构过程中,代码就可能劣化,如果问题出现的速度超过解决的速度,那么将会一直疲于救火,永远无法彻底解决。
新方案中,业务逻辑都放在了 Element 中,ViewController、容器中剩下通用的代码,这部分代码业务同学是没必要去修改,不理解整体也容易改出问题,因此这部分代码由专人来维护,各业务同学有需要改框架层代码的需求,专人来修改。
各 Element 按照业务线划分为独立文件,自己维护的文件可以加 reviewer 或文件变更通知,也可以迁到业务仓库中,进行物理隔离。
日志 & 问题排查
稳定复现的问题,比较容易排查和解决,但概率性的问题,尤其是 iOS 系统问题引起的概率性问题,比较难排查,即便猜测可能引起问题的原因,修改后,也难以自测验证,只能上线再观察。
关键信息提前加日志记录,如用户反馈某个视频有问题,那么需要根据日志,找到相应的 model、Element、View、布局、约束等信息。
信息同步
改动过广,需要及时周知业务线的开发、测试、产品同学,几个方式:
-
拉群通知
-
周会、周报
开发同学最关注的点是什么时候放量、什么时候全量、什么时候可以删掉老代码,不用维护 2 套代码。
其次是改动,框架在不够稳定时,是需要经常改的,如果改动,需要相应受影响的功能的维护同学验证,以及确认测试是否介入。
产品同学也要周知,虽然产品不关注怎么做,但是一旦出问题,没有周知,很麻烦。
保证质量
最重要的是及时发现问题,这是避免或者减少影响的前提条件。
常规的 RD 自测、QA 功能测试、集成测试等是必备的,这里不多说,主要探讨其他哪些手段可以更加及时的发现问题。
新开发的需求,需要开发新、老页面两套代码,同样,也要测试两次,虽然多次强调,但涉及到多个业务线、跨团队、跨职责、时间线长,很容易遗漏,而新页面 ABTest 放量很小,一旦出问题,很难被发现,因此对线上和测试用户区分处理:
-
线上、线下流量策略:线上 AppStore 渠道 ABTest 按数据分析师设计放量;内测、灰度等线下渠道放量 50%,新旧两套各占一半,内测、灰度人员还是有一定规模的,如果是明显的问题,比较容易发现的
-
ABTest 产品指标对照:灰度、线上数据都是有参考价值的,按照 ABTest 数据量,粗评一下是否有问题,如果有明显问题,可及时深入排查
-
Slardar ABTest 技术指标对照:最常用的是 crash 率,对比对照组和实验组的 crash 率,看下是否有新 crash,实验组放量比较小,单独的看 crash 数量是很难发现的,也容易忽略。另外还要别的技术指标,也可以关注下
-
Slardar 技术打点告警配置:重构周期比较长,难以做到每天都盯着,关键位置加入技术打点,系统中配置告警,设置好条件,这样在出现问题时,会及时通知你
-
单元测试:单测是保证重构的必要手段,在框架、SDK 等核心代码,都加入了单测
-
UI 自动化测试:如果有完整的验证用例,可以一定程度上帮助发现问题
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后
以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。
后面我就自己整理了一套资料,还别说,真香!
资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-uWMRPZgs-1713622478095)]
[外链图片转存中…(img-CF1XTgCQ-1713622478096)]
[外链图片转存中…(img-CfxKFBUU-1713622478096)]
[外链图片转存中…(img-kvFDotjf-1713622478097)]
[外链图片转存中…(img-yeR69i9h-1713622478098)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后
以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。
后面我就自己整理了一套资料,还别说,真香!
资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。
[外链图片转存中…(img-plsfj9f4-1713622478099)]
[外链图片转存中…(img-XTzuqiuJ-1713622478100)]
[外链图片转存中…(img-XVplAaTd-1713622478102)]
[外链图片转存中…(img-5YfSDw8F-1713622478103)]
[外链图片转存中…(img-2IsXRs7e-1713622478103)]
[外链图片转存中…(img-wT7nFyNk-1713622478104)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!