你好!

这里是我的数字格纳库,主要分享个人笔记和思考。

关于 Block 与异步重试的设计模式

背景 在开发中,实现一个带重试机制的异步任务是一个常见需求。最近,我在实现一个启动页数据加载的功能时,对重试逻辑的实现方式进行了深入的思考和实践,并踩了一些典型的坑。本文旨在记录两种截然不同的实现思路,分析其优劣,并反思为什么开发者有时会选择一个更复杂甚至错误的方案。 两种实现方案 我们以一个通用的重试辅助方法 executeRetryableTask 为例,对比两种实现。 方案一:复杂的 Block 自引用(有缺陷) 这种方案试图将重试逻辑封装在一个自包含的 Block 单元内,通过 “weak/strong dance” 的变体来避免循环引用。 // 方案一:复杂的 Block 自引用 - (void)executeRetryableTask:(RetryableApiCallBlock)apiCall maxRetries:(NSInteger)maxRetries retryInterval:(NSTimeInterval)retryInterval completion:(RetryCompletionBlock)completion { __block NSInteger currentRetryCount = 0; void (^__block __weak weakAttemptBlock)(void); // 弱引用指针 void (^attemptBlock)(void); // 局部变量,存储在栈上 attemptBlock = ^{ void (^strongAttemptBlock)(void) = weakAttemptBlock; // 试图从弱引用恢复强引用 apiCall(^(id responseObject) { /* ... 成功 ... */ }, ^(NSError *error) { currentRetryCount++; if (currentRetryCount >= maxRetries) { /* ... 最终失败 ... */ } else { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (strongAttemptBlock) { // Bug 所在:strongAttemptBlock 在此会是 nil strongAttemptBlock(); } }); } }); }; weakAttemptBlock = attemptBlock; attemptBlock(); } 方案二:简单的异步递归 这种方案利用简单的递归调用,通过参数传递状态,代码直观且健壮。 ...

June 9, 2025 · 2 min · atomicink

短剧播放器管理设计与实现

从你目前“只能在 ViewController 的滚动逻辑里判断,AVPlayer 只会每个 PlayerView 持有一个”的状态,到能够设计并实现 ShortDramaPlayManager 这样的复杂度和抽象程度,确实需要一个系统性的提升过程。 这不是一蹴而就的,而是经验、知识和刻意练习的积累。下面我将从几个方面为你剖析如何提升: 1. 深化对 Objective-C 和 iOS SDK 的理解 (不仅仅是“会用”) 你列出的核心技能是基础,但要达到 ShortDramaPlayManager 的水平,需要对这些技能有更深的理解和应用: Objective-C Runtime: KVO (Key-Value Observing): ShortDramaPlayManager 通过 KVO 监听 UIScrollView 的 contentOffset 和 AVPlayer 的 timeControlStatus。你需要理解 KVO 的工作原理、如何正确添加和移除观察者(注意 context 的使用、@try-@catch 避免崩溃)、以及潜在的陷阱(如父类也观察了同一个属性、忘记移除观察者导致的崩溃)。 消息传递: 虽然不直接体现在这个 Manager 中,但理解消息传递机制有助于你设计更灵活的接口和回调。 Blocks: Manager 中大量使用了 block(例如 NSTimer 的 target-action 模式可以用 block 替代,GCD 的使用)。你需要熟练掌握 block 的语法、内存管理(__weak, __strong self)、以及作为参数和返回值的使用。 内存管理 (ARC/MRC): 强引用循环 (Retain Cycles): NSTimer 持有 target,如果 target 也强持有 timer,就会循环引用。KVO、NSNotificationCenter 的观察者也可能导致循环引用。Manager 中通过在 dealloc 和特定时机移除观察者、停止 timer 来避免。你需要培养对潜在循环引用的敏感度。 对象生命周期: 理解对象的创建、持有、释放过程,尤其是在集合类(NSDictionary, NSMutableSet)中管理对象时。 iOS SDK: AVFoundation: AVPlayer vs AVPlayerItem vs AVPlayerLayer: Manager 的核心是共享一个 AVPlayer 实例,通过切换 AVPlayerItem 来播放不同视频,并将 AVPlayer 关联到不同 VideoPlayerView 的 AVPlayerLayer。这是为了性能和资源优化。你需要理解这三者的关系和各自职责。 AVPlayerItem 状态和通知: 如何预加载 AVPlayerItem (preparePlayerView:withURL:),如何监听播放结束 (AVPlayerItemDidPlayToEndTimeNotification) 并处理(如重播)。 UIScrollView: contentOffset, contentInset, bounds: 精确计算视图可见性、相对位置的基础。 Delegate vs KVO: Manager 选择 KVO 监听 contentOffset,而不是依赖 delegate。这使得 Manager 可以独立于 ViewController,并且可以同时管理多个 UIScrollView。 NSNotificationCenter: 用于应用生命周期事件(前后台切换)的响应,以及可能的内部事件通知。 NSTimer: 用于实现延迟操作(滚动停止后播放、滚动检测)。注意 timer 的精度问题、RunLoop 模式,以及及时 invalidate。 UIView 坐标转换: convertRect:toView: 和 convertPoint:fromView: 对于判断视图在屏幕上的位置至关重要 (isPlayerViewInReasonableRange)。 2. 学习和应用设计模式与原则 ShortDramaPlayManager 体现了多种设计模式和原则: ...

May 30, 2025 · 3 min · atomicink

YYWebImage 在 Masonry 宽度不符预期的问题

问题场景 假设我们有如下布局代码,目标是在一个容器 logoTitleContainer 内居中显示一个 Logo 图片 _appLogoImageView,并指定其高度为 20 points: UIView *logoTitleContainer = [[UIView alloc] init]; logoTitleContainer.backgroundColor = [UIColor clearColor]; [_containerView addSubview:logoTitleContainer]; [logoTitleContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(_containerView); make.top.mas_offset(40); make.height.mas_equalTo(20); make.width.mas_equalTo(192); }]; // Logo ImageView _appLogoImageView = [[UIImageView alloc] init]; _appLogoImageView.contentMode = UIViewContentModeScaleAspectFit; [logoTitleContainer addSubview:_appLogoImageView]; [_appLogoImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(20); make.centerY.equalTo(logoTitleContainer); make.centerX.equalTo(logoTitleContainer); make.width.lessThanOrEqualTo(logoTitleContainer); }]; // 加载网络图片 (原始尺寸 210x44 像素) NSURL *imageUrl = [NSURL URLWithString:@"YOUR_IMAGE_URL_HERE"]; [_appLogoImageView yy_setImageWithURL:imageUrl placeholder:nil]; 预期效果: _appLogoImageView 的高度被固定为 20 points,ScaleAspectFit 模式会根据图片原始宽高比(210/44 ≈ 4.77)计算出适应此高度的宽度(20 * 4.77 ≈ 95.4 points),图片应该以此尺寸显示在容器中心。 ...

April 29, 2025 · 2 min · atomicink

Protocol 实践思维养成

很多开发者(包括经验丰富的)都会遇到类似情况:明明知道某个技术概念(比如 Protocol),但在实际编码时却想不起来用。这其实是一个非常普遍的学习阶段现象。 一、为什么知道 Protocol 却想不起用? 1. 知识与应用存在断层 知道 ≠ 会用:学习技术概念时,我们往往停留在"知道它是什么",但缺乏"什么时候用它"的直觉。 示例:就像知道螺丝刀的存在,但看到松动的螺丝时,第一反应可能是用手去拧,而不是找螺丝刀。 2. 惯性思维主导 习惯性用 if-else:面对具体问题时,大脑会优先调用最熟悉的解决方案(比如条件分支),而不是评估所有可能性。 思维路径:“我需要判断不同类 → 用 isKindOfClass: → 写一堆 if-else"而不是:“这些类有共同行为 → 应该抽象成协议”。 3. 缺乏设计模式的敏感度 Protocol 的本质是抽象行为约定,但初学者往往更关注具体实现细节(比如如何写方法),而忽略抽象设计。 4. 对 Protocol 的理解不够深刻 可能你目前对 Protocol 的认知停留在: “它是声明方法的列表”(表面) 而不是:“它是解耦和扩展架构的工具”(本质)。 二、如何培养「用 Protocol」的思维? 1. 建立「行为抽象」的条件反射 遇到以下场景时,强制自己思考 Protocol: 多个类有相同的方法名(比如你的 incrementShareCount)。 需要判断对象是否能响应某个方法(respondsToSelector:)。 代码中出现 if ([obj isKindOfClass:[XXX class]])。 需要跨模块通信(如 Delegate 模式)。 口诀:“重复方法?抽象它!类型判断?协议化!跨类调用?定义协议!” 2. 从模仿优秀代码开始 研究 Apple 官方代码:iOS SDK 中大量使用 Protocol(如 UITableViewDataSource, NSCopying)。思考为什么这些场景要用 Protocol。 示例: // 为什么 UITableView 不直接提供数据,而是通过 DataSource 协议? // 答案:解耦!UITableView 不需要知道数据来源是数组、CoreData 还是网络。 3. 重构练习:将现有代码改为 Protocol 实现 具体步骤: ...

April 2, 2025 · 2 min · atomicink

ijkplayer iOS设置代理

IJKFFOptions *op = [IJKFFOptions optionsByDefault]; [op setFormatOptionValue:@"http://127.0.0.1:80" forKey:@"http_proxy"];

June 4, 2024 · 1 min · atomicink

Pod 关于 unknown UUID 警告

[!] `<PBXSourcesBuildPhase UUID=`BC2BAE431A89F1450060D863`>` attempted to initialize an object with an unknown UUID. `38F3FD552A2F25DF00AEAAC4` for attribute: `files`. This can be the result of a merge and the unknown UUID is being discarded. 这个错误通常是由于合并代码时导致的,可能会导致未知的UUID被丢弃。解决方法如下: 从CocoaPods中解除你的项目。删除Xcode项目中所有CocoaPods的痕迹。 $ pod deintegrate 再次安装Pod。 $ pod install

March 4, 2024 · 1 min · atomicink

UITextView限制字数

一开始认为UITextView限制字数很简单,在delegate里处理一下shouldChangeTextInRange就行了: //const NSInteger kMaxLength = 100; - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { return textView.text.length + text.length - range.length <= kMaxLength; } 没想到在使用拼音输入法的联想功能时并不触发这个方法。只好在textViewDidChange里强行限制了: - (void)textViewDidChange:(UITextView *)textView { if(textView.text.length > kMaxLength) { textView.text = [textView.text substringToIndex:kMaxLength]; } }

December 15, 2015 · 1 min · atomicink

命令行打包ipa

入职新公司,公司的自动打包环境最近出问题了,只好手动打包,命令如下: xcodebuild -scheme scheme名 -sdk iphoneos -configuration AppStoreDistribution archive -archivePath $PWD/build/app名.xcarchive xcodebuild -exportArchive -archivePath $PWD/build/app名.xcarchive -exportPath $PWD/build -exportProvisioningProfile "Provisioning名" 注意:以前流行的xcrun PackageApplication现在是无法使用的。

October 31, 2015 · 1 min · atomicink

NSOrderedSet的崩溃问题

最近把项目里CoreDataModel的一个一对多的关系改为有序的,对象的class因此从NSSet变为NSOrderedSet。 @property (nonatomic, retain) NSOrderedSet *myFlowDetail; - (void)addMyFlowDetailObject:(MyFlowDetailEntity *)value; 使用中却发现向NSOrderedSet里添加对象时会崩溃,经查询发现似乎是一个Apple从2011年到现在都没解决的bug。 临时解决方法是给这个类添加一个category覆盖上述方法: - (void)addMyFlowDetailObject:(MyFlowDetailEntity *)value { NSMutableOrderedSet *flowDetail = [[NSMutableOrderedSet alloc] initWithOrderedSet:self.myFlowDetail]; [flowDetail addObject:value]; self.myFlowDetail = flowDetail; } 参考:Exception thrown in NSOrderedSet generated accessors

March 13, 2015 · 1 min · atomicink

《把你的英语用起来》阅读笔记

核心策略 专注学习: 避免社交网络等干扰。 自我监控: 通过记笔记追踪进度。 循序渐进: 从小目标开始,逐步增加难度。 立即行动: 克服拖延,想到就做。 正面激励: 采用奖励机制,避免自我批评。 基本原则 告别电视剧: 减少被动娱乐时间。 晨间学习: 将学习时间调整至精力更充沛的早上。 时间管理: 高效结合零碎时间与整块时间。 听力提升 基础发音: 赖世雄音标: 看书听CD,大声跟读模仿。 American Pronunciation Workshop & 剖面 American Accent Training 进阶听力与模仿 (EnglishPod): 前提: 确认已超越VOA慢速水平。 计划: 制定每日学习与复习计划。 步骤: 听讲解: 1-2遍,理解为主。 听对话: 专注听约9遍。 跟读语调: 模仿录音韵律,哼唱跟读。 跟读练习: 先看文本跟读3-4遍,再脱稿连续跟读约9遍。 笔记与复习: 记录生词重点,利用零碎时间记忆。 强化练习: 录音对比: 挑选喜欢的课文录音,模仿语气,与原音穿插对比听9次。 迭代优化: 不满意则重复跟读与录音步骤。 泛听材料: Audiobook (有声读物) 语法巩固 核心教材: 《语法俱乐部》: 精读至少5遍,结合大量阅读实践,分阶段(初学、数月后、隔年后)反复研读,打好内功基础。 补充练习: 《剑桥中级英语语法》(英文版): 在《语法俱乐部》后使用,通过真实语料库例句检验和巩固语法知识。 阅读方法 (透析法) 控制查词: 每2页最多查1个生词。 每日复习: 每天花5分钟复习近几天查过的词。 集中突破 (可选): 如进行“7天行动”,则每天读40页,坚持7天。 读后回顾: 阅读完成后进行总结。

August 3, 2013 · 1 min · atomicink