[{"content":"前言 TTGEmojiRate 是我很早之前写的一个评分控件。最初的灵感来自一个 Dribbble 动画：用户上下拖动 Emoji，表情从不开心慢慢变成开心，评分也跟着变化。相比星星评分，这种方式更像是在表达一个情绪。\n这次整理到 1.0.0，不是简单把 Swift 版本升级一下，而是把原来“一个会变嘴型的表情评分”扩展成了一个更完整的情绪评分组件：范围、步进、点击评分、只读展示、表情预设、眼睛/嘴型样式、脸型、渐变、填充、自定义路径都补了进来。😊\nGithub: https://github.com/zekunyan/TTGEmojiRate\n更新 1.0.0 的主要变化：\n评分范围可配置：不再固定 0 到 5，可以设置 minimumRateValue / maximumRateValue。 步进控制：rateStep = 0 表示连续评分，也可以设置 0.5 或 1 做半星/整数评分。 交互方式增强：支持拖动、点击评分、只读展示，以及垂直/水平手势方向。 表情系统扩展：新增 expression preset、eye style、mouth style 和 accessories。 脸型和填充：支持 circle、roundedSquare、squircle、blob、capsule、shield、cloud 等脸型，以及填充和渐变。 自定义路径：可以替换内置绘制，也可以在内置 Emoji 上 overlay 自己的路径。 Objective-C 兼容：枚举和 setter 都提供了 ObjC-friendly 入口。 快速使用 基础用法还是创建一个 EmojiRateView，设置评分范围，然后监听变化：\nimport TTGEmojiRate let rateView = EmojiRateView(frame: CGRect(x: 0, y: 0, width: 220, height: 220)) rateView.minimumRateValue = 0 rateView.maximumRateValue = 10 rateView.rateStep = 0 rateView.rateValueChangeCallback = { score in print(\u0026#34;Score:\u0026#34;, score) } view.addSubview(rateView) 如果产品需要更明确的分值，比如 1 到 5 分、每次 0.5：\nrateView.minimumRateValue = 1 rateView.maximumRateValue = 5 rateView.rateStep = 0.5 rateView.setRateValue(4.5, animated: true) 这个地方要注意：连续值适合情绪输入、满意度滑动；步进值适合问卷、评价、统计。控件提供两种方式，但具体选哪个还是要看业务。\n表情和样式 以前的版本主要是调整嘴巴弧度和颜色变化。新版里，表情本身变得更丰富：\nrateView.setEmojiExpressionPreset(.playful) rateView.setEmojiEyeStyle(.heart) rateView.setEmojiMouthStyle(.openSmile) rateView.emojiShowAccessories = true 脸型、填充和渐变也可以单独控制：\nrateView.faceShape = TTGEmojiRateFaceShape.blob.rawValue rateView.faceShapeMorphEnabled = true rateView.faceFillEnabled = true rateView.faceFillGradientEnabled = true rateView.rateGradientColorEnabled = true rateView.rateGradientColors = [ .systemCyan, .systemBlue, .systemPurple ] 这里的设计思路还是沿用旧版：rateValue 是核心状态，表情、颜色、路径都根据当前 progress 重新计算。只是现在可参与计算的维度更多了，不再只有嘴巴和线条颜色。\n自定义路径 如果内置 Emoji 不符合产品气质，可以用自定义路径替换，或者保留内置表情再叠加额外图形：\nrateView.customEmojiPathMode = TTGEmojiRateCustomPathMode.replace.rawValue rateView.customEmojiPathProvider = { rect, progress, view in let path = UIBezierPath() let faceRect = rect.insetBy(dx: 12, dy: 12) path.append(UIBezierPath(ovalIn: faceRect)) let symbol = UIBezierPath() let midY = rect.midY + (1 - progress) * 20 symbol.move(to: CGPoint(x: rect.minX + 54, y: midY)) symbol.addLine(to: CGPoint(x: rect.midX, y: midY - 18)) symbol.addLine(to: CGPoint(x: rect.maxX - 54, y: midY)) path.append(symbol) return path } 自定义路径这里我比较喜欢保留两种模式：\nreplace：完全替换内置绘制，适合品牌化很强的控件。 overlay：保留内置 Emoji，再叠加自己的符号、波形、徽章等。 这样可以避免“想加一点点元素，却被迫重写所有绘制”的问题。\n交互方向和只读状态 旧版主要是上下拖动。这次增加了水平手势方向，也补了 tap-to-rate 和 read-only：\nrateView.rateGestureDirection = TTGEmojiRateGestureDirection.horizontal.rawValue rateView.isTapToRateEnabled = true rateView.isReadOnly = true rateView.setRateColor(from: .systemBlue, to: .systemGreen) 只读状态看起来是小功能，但实际挺常见：详情页展示平均分、评价列表展示用户打过的分、统计页展示结果，都不应该还能被拖动修改。把输入态和展示态放在同一个控件里，会比再写一个只读 View 简单。\n实现小结 从实现上看，TTGEmojiRate 仍然是基于 CAShapeLayer 和 UIBezierPath：\n拖动或点击改变 rateValue。 rateValue 归一化成 rateProgress。 根据 progress 计算嘴型、眼睛、脸型、颜色、填充和自定义路径。 更新 shape layer，并在 setRateValue(_:animated:) 里做 path/color 动画。 旧版文章里讲过 HSB 颜色插值，这个思路新版继续保留。相比直接在 RGB 上插值，HSB 更适合做“情绪从低到高”的颜色变化，因为 hue 的连续性更符合直觉。\n总结 TTGEmojiRate 1.0.0 的方向是把评分从“选一个数字”变成“表达一个情绪”。它不一定适合所有评分场景，比如严肃的表单可能还是星星或分段控件更清楚；但在满意度、轻量反馈、活动页、情绪记录这类场景里，一个会动的表情会更有温度。\n总的来说，这次更新把旧版的绘制思路保留下来，同时补齐了现代项目需要的可配置性和展示状态。接下来如果继续优化，我会更关注示例、无障碍和更多产品化场景里的默认参数。\n","date":"2026-07-11T00:00:00Z","image":"/assets/2026/07/11/ttgemojirate-1-0-0/promo_poster.png","permalink":"/2026/07/11/ttgemojirate-1-0-0/","title":"TTGEmojiRate 1.0.0：让评分控件重新有表情"},{"content":"前言 TTGPuzzleVerify 是一个拼图验证控件。最早写它的时候，重点是“怎么从图片里抠出一块拼图、怎么做镂空、怎么判断拖动是否成功”。那篇旧文章里也主要讲的是 UIBezierPath、CAShapeLayer.mask 和手势判断。\n这次更新到 2.0.0，核心思路没有变，但项目本身变得完整了很多：Swift 原生实现、Objective-C 可调用、SwiftUI 可包装、统一配置对象、更多验证行为、状态机、失败反馈、结果指标等等。\nGithub: https://github.com/zekunyan/TTGPuzzleVerify\n更新 2.0.0 这次主要做了这些事情：\n迁移到 Swift：核心类 TTGPuzzleVerifyView 用 Swift 实现，同时通过 @objc 暴露给 Objective-C。 统一配置：新增 TTGPuzzleVerifyConfiguration，把形状、尺寸、容差、拖动方向、自动吸附、重试次数等放到一起。 更多形状：内置 classic、square、circle，并支持自定义 UIBezierPath。 拖动方向控制：支持水平、垂直、双轴拖动。 状态机：公开 idle、dragging、verified、failed、locked 状态。 结果指标：成功/失败回调里带上偏移、耗时、拖动距离、交互次数等数据。 SwiftUI / UIKit / Objective-C 共用核心：不同集成方式不再各写一套逻辑。 快速使用 UIKit 里最基本的配置如下：\nimport TTGPuzzleVerify let puzzleView = TTGPuzzleVerifyView() puzzleView.image = UIImage(named: \u0026#34;background\u0026#34;) let configuration = TTGPuzzleVerifyConfiguration() configuration.puzzlePattern = .circlePattern configuration.puzzleSize = CGSize(width: 92, height: 92) configuration.allowedAxes = .horizontal configuration.verificationTolerance = 7 puzzleView.applyConfiguration(configuration) puzzleView.puzzleBlankPosition = CGPoint(x: 210, y: 20) puzzleView.resetVerification() 如果在 SwiftUI 里用，也就是包一层 UIViewRepresentable：\nstruct PuzzleVerifyRepresentable: UIViewRepresentable { @Binding var isVerified: Bool func makeUIView(context: Context) -\u0026gt; TTGPuzzleVerifyView { let view = TTGPuzzleVerifyView() view.image = UIImage(named: \u0026#34;background\u0026#34;) let configuration = TTGPuzzleVerifyConfiguration() configuration.puzzlePattern = .classicPattern configuration.puzzleSize = CGSize(width: 86, height: 86) configuration.allowedAxes = .both configuration.autoSnapWhenWithinTolerance = true view.applyConfiguration(configuration) view.puzzleBlankPosition = CGPoint(x: 220, y: 96) view.verificationChangeBlock = { _, verified in isVerified = verified } return view } } 配置对象 旧版很多属性是直接设置到 view 上的，这当然简单，但当行为越来越多以后，调用处会比较散。新版增加了 TTGPuzzleVerifyConfiguration：\nlet configuration = TTGPuzzleVerifyConfiguration() configuration.puzzlePattern = .classicPattern configuration.puzzleSize = CGSize(width: 100, height: 100) configuration.verificationTolerance = 8 configuration.allowedAxes = .both configuration.autoSnapWhenWithinTolerance = true configuration.recordsTrack = true configuration.maxRetryCount = 3 puzzleView.applyConfiguration(configuration) 这里最重要的几个参数：\npuzzlePattern：拼图形状。 puzzleSize：拼图块大小。 verificationTolerance：验证容差，避免要求用户像素级对齐。 allowedAxes：限制水平、垂直或双轴拖动。 autoSnapWhenWithinTolerance：进入容差范围时自动吸附到正确位置。 maxRetryCount：失败次数达到上限后进入 locked 状态。 这个配置对象的好处是，产品层可以维护几套不同策略：比如登录页更严格，活动页更轻一点，或者调试环境打开 track 记录。\n状态和结果 拼图验证不只是“成功/失败”两个布尔值。实际接到业务里以后，常常还需要知道失败了几次、拖动过程是否异常、耗时多久、偏差是多少。\n新版公开了状态：\npublic enum TTGPuzzleVerifyState: Int { case idle case dragging case verified case failed case locked } 完成时会产生 TTGPuzzleVerifyResult，里面包括：\nisVerified puzzlePosition blankPosition xOffset / yOffset elapsedTime dragDistance interactionCount 注意，这些指标不是为了在客户端做“安全风控”，客户端本身不适合承担这个职责。它更适合用于体验分析、调试、轻量行为判断，真正的安全策略还是应该服务端参与。\n实现回顾 拼图控件核心还是三个层：\n背景图：完整图片。 镂空图：用 even-odd mask 从背景中抠掉拼图区域。 拼图块：同一张图片通过 puzzle path mask 裁出来。 旧文章里讲过 mask 的基本思路，这次重构后，重点是把这些能力封装成可以稳定复用的状态机：配置变化时更新 path 和 mask，拖动时更新拼图位置，结束时根据容差和轴向策略决定 verified/failed/locked。\n在 API 上，customPuzzlePatternPath 仍然保留给自定义形状：\nconfiguration.puzzlePattern = .customPattern puzzleView.customPuzzlePatternPath = myBezierPath puzzleView.applyConfiguration(configuration) 这样内置形状不够时，外部可以继续提供自己的 UIBezierPath，控件本身只负责把路径缩放、移动、应用到 mask 和验证逻辑里。\n总结 TTGPuzzleVerify 2.0.0 更像是把一个“有趣的拼图 Demo 控件”整理成了一个能在项目里使用的组件：Swift 化、配置化、状态清晰、回调信息更完整，也兼顾了 UIKit、SwiftUI 和 Objective-C。\n当然，拼图验证这种东西也要看使用场景。它可以增加交互门槛、减少误触或低成本刷操作，但不要把它当成完整安全方案。控件负责交互体验，安全策略还是要回到整体系统设计里。\n","date":"2026-07-11T00:00:00Z","image":"/assets/2026/07/11/ttgpuzzleverify-2-0-0/promo_poster.jpg","permalink":"/2026/07/11/ttgpuzzleverify-2-0-0/","title":"TTGPuzzleVerify 2.0.0：拼图验证控件的 Swift 化和能力补齐"},{"content":"前言 TTGSnackbar 最早只是我用 Swift 写的一个 Android Snackbar 风格提示条：显示一段文案，带一个 Action，几秒后自动消失。\n这次整理到 2.0.0，目标变成了一个更完整的 iOS 消息反馈组件：不仅能显示顶部/底部消息，还要支持语义样式、队列管理、自定义 View、Swift concurrency、无障碍、触觉反馈和现代 Apple 平台要求。\nGithub: https://github.com/zekunyan/TTGSnackbar\n更新 这次 2.0.0 可以理解成几个方向的补齐：\n平台升级：当前开发目标为 Swift 5.9、Xcode 15+、iOS 16.0+。 语义样式：内置 .default、.info、.success、.warning、.error、.loading。 配置对象：新增 TTGSnackbarConfiguration，更适合现代 Swift 调用。 Swift Concurrency：可以 await 用户点击、滑动、dismiss、展示失败等结果。 队列管理：TTGSnackbarManager 支持排队、替换当前、相同消息去重。 体验细节：Dynamic Type、VoiceOver、Reduce Motion、haptics、生命周期暂停/恢复计时器。 自定义能力：支持自定义内容 View，也可以放到自定义容器里展示。 快速使用 最简单的用法还是没有变：\nimport TTGSnackbar let snackbar = TTGSnackbar(message: \u0026#34;TTGSnackbar!\u0026#34;, duration: .short) snackbar.show() 如果消息需要一个恢复入口，比如删除文件后的 Undo：\nlet snackbar = TTGSnackbar( message: \u0026#34;File deleted\u0026#34;, duration: .middle, actionText: \u0026#34;Undo\u0026#34; ) { snackbar in restoreFile() snackbar.dismiss() } snackbar.show() 这个 API 继续保留，是为了兼容老项目，也因为它确实足够直接。对一个提示控件来说，简单场景不应该被复杂 API 绑架。\n语义样式 以前做不同状态的提示，通常是自己设置背景色、文字色、icon。写多了之后就会发现，业务里其实反复出现的是几类固定语义：成功、警告、错误、加载中。\n所以新版加了 TTGSnackbarStyle：\nlet snackbar = TTGSnackbar(message: \u0026#34;Saved successfully\u0026#34;, duration: .middle) snackbar.style = .success snackbar.show() 加载中的场景也可以直接用 .loading，配合 .forever 手动结束：\nlet snackbar = TTGSnackbar(message: \u0026#34;Uploading...\u0026#34;, duration: .forever) snackbar.style = .loading snackbar.actionText = \u0026#34;Cancel\u0026#34; snackbar.actionBlock = { snackbar in cancelUpload() snackbar.dismiss() } snackbar.show() Configuration API 属性式配置比较直观，但是当参数多起来以后，会出现“创建对象、设置一堆属性、最后 show”的散落代码。2.0.0 里增加了 TTGSnackbarConfiguration，把一次展示需要的信息收拢到一个 value object：\nlet snackbar = TTGSnackbar(configuration: .init( message: \u0026#34;File deleted\u0026#34;, duration: .long, style: .warning, actionText: \u0026#34;Undo\u0026#34;, actionBlock: { snackbar in undoDelete() snackbar.dismiss() } )) snackbar.show() 这不是为了替代旧 API，而是给复杂一点的调用场景一个更稳定的入口。比如产品里做统一消息中心、错误处理器、网络状态提示时，用 configuration 传递会清楚很多。\nasync / await Snackbar 其实是一个临时 UI，但它经常和业务决策有关：用户点了 Undo 还是直接忽略？展示失败要不要降级？被队列丢弃算不算完成？\n新版可以直接等待结果：\nlet result = await TTGSnackbar.show(configuration: .init( message: \u0026#34;File deleted\u0026#34;, duration: .long, style: .warning, actionText: \u0026#34;Undo\u0026#34; )) switch result { case .action: undoDelete() case .dismissed: break case .dropped: print(\u0026#34;A manager policy dropped the snackbar\u0026#34;) case .failedToPresent: print(\u0026#34;No window or container was available\u0026#34;) default: break } 这块实现上用的是 continuation，把 action、second action、tap、swipe、dismiss、manager drop、presentation failure 都统一成 TTGSnackbarResult。调用方不用再在几个 block 之间跳来跳去，读起来会顺很多。\n自定义内容 有些提示不只是“一行文字 + 一个按钮”，比如同步状态、表单错误、网络诊断，就需要自己塞一个 View 进去：\nlet contentView = UIStackView() contentView.axis = .vertical contentView.spacing = 4 contentView.isLayoutMarginsRelativeArrangement = true contentView.layoutMargins = UIEdgeInsets(top: 16, left: 18, bottom: 16, right: 18) let titleLabel = UILabel() titleLabel.text = \u0026#34;Custom content view\u0026#34; contentView.addArrangedSubview(titleLabel) let snackbar = TTGSnackbar(customContentView: contentView, duration: .forever) snackbar.shouldActivateLeftAndRightMarginOnCustomContentView = true snackbar.show() 这个设计里，库继续负责安全区域、展示位置、动画和 dismiss，内容内部怎么布局交给业务。边界要分清，不然控件会越来越像一个小页面 =。=\n总结 TTGSnackbar 2.0.0 最大的变化不是“样式变好看了”，而是从一个简单提示条，补成了一个更适合现代 iOS 项目的反馈组件。\n简单场景仍然可以一行 show()；复杂场景可以用 configuration、manager 和 async result 做统一管理。这个平衡比较重要：提示控件要轻，但也不能在真正接到业务流程时马上露怯。\n","date":"2026-07-11T00:00:00Z","image":"/assets/2026/07/11/ttgsnackbar-2-0-0/ttgsnackbar-poster.jpg","permalink":"/2026/07/11/ttgsnackbar-2-0-0/","title":"TTGSnackbar 2.0.0：从简单提示条到现代消息反馈组件"},{"content":"前言 TTGTagCollectionView 是我很早之前写的标签布局控件。最开始的目标很简单：项目里总是要做“标签云”、“筛选项”、“兴趣标签”这种 UI，不想每次都重新计算换行和选中状态，所以把这块抽出来做了一个库。\n这次整理到了 3.3.0，变化就不只是补几个属性了，而是把它重新整理成了一个 Swift-first、Objective-C 仍然可用、布局和视图分离 的版本。简单来说，老项目能继续用，新项目也可以更自然地接入 Swift / SPM / AutoLayout。\nGithub: https://github.com/zekunyan/TTGTagCollectionView\n更新 这次 3.3.0 我自己比较关心的是下面这些点：\nSwift-first：核心源码迁移到 Swift，Package product 为 TTGTags，同时保留 Objective-C 兼容命名。 模型化的文字标签：TextTag 拆成 content、style、selectedStyle、attachment，外部更容易管理状态。 更强的布局能力：垂直换行、水平滚动、多行限制、6 种 alignment、水平分布和垂直对齐都集中到了布局层。 AutoLayout 友好：支持 intrinsicContentSize、preferredMaxLayoutWidth，放到 StackView、自适应 Cell、表单里更省心。 性能更可控：文字测量缓存和纯布局缓存拆出来，密集标签列表不用每次都全量重新算。 交互补齐：除了点击和选中，还加了滑动选择、长按拖拽排序、拖到删除区域等常见需求。 快速使用 现在的使用方式更像“先构造一个 tag 模型，再交给 collection view 布局”：\nimport TTGTags let tagView = TextTagCollectionView() tagView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tagView) let content = TextTagStringContent(text: \u0026#34;Swift\u0026#34;) content.textFont = .boldSystemFont(ofSize: 14) content.textColor = .white let style = TextTagStyle() style.enableGradientBackground = true style.gradientBackgroundStartColor = .systemBlue style.gradientBackgroundEndColor = .systemPurple style.cornerRadius = 12 style.extraSpace = CGSize(width: 14, height: 8) let tag = TextTag(content: content, style: style) tagView.add(tag: tag) tagView.reload() 如果只做简单文字标签，这个模型看起来比以前多了一层，但好处是后面扩展不会把 TextTagCollectionView 自己撑得越来越大。文字、样式、选中样式、附件数据各管各的，调用方也更容易做 diff、缓存或者复用。\n样式更新 以前标签样式基本是一些直接挂在 View 上的属性。现在统一放到了 TextTagStyle：\nlet selectedStyle = TextTagStyle() selectedStyle.backgroundColor = .systemOrange selectedStyle.cornerRadius = 12 selectedStyle.extraSpace = CGSize(width: 14, height: 8) tag.selectedStyle = selectedStyle tagView.selectionLimit = 3 TextTagStyle 里面包含背景色、渐变背景、圆角、边框、阴影、padding、最大/最小/固定宽高等属性。这个设计的重点不是属性数量，而是 每个 Tag 可以有自己的 style，不再只能让整个 collection 共享一套样式。\n布局层拆出来 这次重构里比较重要的一点，是把布局计算抽成了 TagCollectionLayout。它接收 tag sizes、滚动方向、alignment、spacing、inset、容器宽高等输入，然后输出每个 tag 的 frame 和最终 contentSize。\n这样做有几个好处：\n视图层只负责把 frame 应用到子 view 上，代码职责更清楚。 布局计算可以单独写单元测试，不需要依赖 UIKit 交互过程。 对相同输入可以做布局缓存，避免频繁刷新时重复计算。 比如下面这个配置，可以从普通换行标签切到水平滚动的筛选条：\ntagView.alignment = .fillByExpandingWidth tagView.horizontalSpacing = 8 tagView.verticalSpacing = 8 tagView.contentInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) tagView.scrollDirection = .horizontal tagView.numberOfLines = 2 tagView.showsHorizontalScrollIndicator = false tagView.reload() 交互能力 标签控件经常一开始只是展示，后来就会慢慢长出“多选”、“滑动选择”、“排序”、“删除”之类的需求。3.3.0 这次把这些常用交互也补齐了一些：\ntagView.enableTagSelection = true tagView.enableSwipeSelection = true tagView.enableTagReordering = true tagView.enableDragToDelete = true tagView.selectionLimit = 3 对应的 delegate 里可以拦截点击、滑动选择、移动、删除：\nfunc textTagCollectionView(_ collectionView: TextTagCollectionView, canTapTag tag: TextTag, at index: Int) -\u0026gt; Bool { true } func textTagCollectionView(_ collectionView: TextTagCollectionView, didMoveTag tag: TextTag, fromIndex: Int, toIndex: Int) { print(\u0026#34;move\u0026#34;, fromIndex, toIndex) } 这类功能放在库里并不是为了替业务层做决定，而是把手势、快照、自动滚动、frame 重排这些重复工作处理掉。业务层只需要决定“能不能选”、“能不能删”、“移动后数据源怎么更新”。\n架构 从架构上看，新的版本大致分成几块：\nTextTag / TextTagContent / TextTagStyle：描述标签数据和外观。 TextTagCollectionView：文字标签的高层封装，处理交互和状态。 TagCollectionView：通用自定义 View 标签容器。 TagCollectionLayout：纯布局计算。 TextTagComponentView：负责最终渲染，包括渐变、圆角、边框、阴影等。 这个拆法比以前多了几个类型，但每个类型的职责更窄。后续如果继续加 SwiftUI 包装、自定义内容、测试用例，也不会都堆到一个类里。\n总结 TTGTagCollectionView 3.3.0 更像是一次“把老项目重新整理成现代 iOS 组件”的更新。它保留了最初“简单显示标签”的入口，也补上了 Swift、SPM、AutoLayout、缓存、交互和 Objective-C 兼容这些实际项目里会遇到的问题。\n如果项目里只是几个固定标签，自己写当然也可以；但如果标签数量多、样式多、还要放在 Cell 或筛选页里反复复用，用一个专门的布局控件会省掉不少琐碎工作。嗯，这也是这个库一开始存在的原因。\n","date":"2026-07-11T00:00:00Z","image":"/assets/2026/07/11/ttgtagcollectionview-3-3-0/promo_poster.png","permalink":"/2026/07/11/ttgtagcollectionview-3-3-0/","title":"TTGTagCollectionView 3.3.0：Swift-first 的标签布局控件更新"},{"content":"前言 这次是一个全新的项目：TTGRefresh。\niOS 里下拉刷新、上拉加载更多是非常常见的需求。自己写并不难，但真正放到项目里以后，会慢慢遇到一些细节：contentInset 怎么恢复、刷新和加载更多能不能同时触发、异步任务结束时 UI 怎么收起、列表内容不满一屏时 footer 怎么处理、想换一个动画样式是不是要改很多业务代码。\nTTGRefresh 的目标就是把这些细节收拢起来，同时保持 API 尽量轻：通过一个 scrollView.ttg 命名空间，把 header refresh、footer refresh、infinite load、默认 UI、路径动画和自定义 View 都接进去。\nGithub: https://github.com/zekunyan/TTGRefresh\n项目特点 TTGRefresh 0.1.0 目前是第一个版本，我希望它一开始就把方向定清楚：\n极简 API：常用场景一两行代码完成。 低侵入性：不需要替换 UITableView / UICollectionView，只通过 scrollView.ttg 扩展安装能力。 高度可定制：默认文字 UI、20 种内置 path effect、或者完全自定义 UIView 都可以接入。 状态机明确：header 和 footer 都有独立状态，刷新、加载、结束、无更多数据都可控。 异步友好：支持 callback 和 async/await，并且可配置异步结束后自动收起。 避免重复触发：内部有 activity coordinator，默认拦截重复刷新、加载更多互相打架这类问题。 开箱即用动画样式：内置 20 个路径动画样式，先能用、再定制。✨ 快速使用 最常见的下拉刷新：\nimport TTGRefresh tableView.ttg.addHeaderRefresh { await viewModel.reload() } 上拉加载更多：\ntableView.ttg.addFooterRefresh { await viewModel.loadNextPage() } tableView.ttg.endFooterRefreshingWithNoMoreData() tableView.ttg.resetFooterNoMoreData() 如果是 feed 流，希望滚动接近底部时自动加载下一页：\ncollectionView.ttg.addInfiniteLoad(preloadOffset: 180) { await viewModel.loadNextPage() } 这个 API 的重点是 scrollView.ttg。它不会要求你换一个列表基类，也不会把刷新逻辑藏到 ViewController 之外。它只是给已有的 UIScrollView 增加一个很薄的入口，业务代码仍然围绕原本的 tableView / collectionView 写。\nProgrammatic Control 刷新控件不能只响应手势，很多页面会在首次进入时自动刷新，或者在筛选条件变化后主动重新加载。这类入口也走同一套状态机：\ntableView.ttg.triggerRefresh() tableView.ttg.triggerLoadMore() tableView.ttg.beginHeaderRefreshing(animated: true, triggerAction: true) tableView.ttg.beginFooterRefreshing(animated: true, triggerAction: true) tableView.ttg.endHeaderRefreshing() tableView.ttg.endFooterRefreshing() 这样手动触发和用户手势触发的表现是一致的：动画、状态、回调、防重入都走同一条路径。这个地方如果分开写，后面很容易出现“首次加载正常，手动下拉又是另一套状态”的问题。\n内置动画样式 TTGRefresh 内置了 20 种 path effect 样式，比如 auroraWave、pulseCircuit、quantumKnot、rocketOrbit 等。每个样式都可以同时作为 refresh header、pull load-more footer 或 infinite-load footer。\ntableView.ttg.addPathEffectHeaderRefresh(style: .auroraWave) { await viewModel.reload() } tableView.ttg.addPathEffectFooterRefresh(style: .rocketOrbit) { await viewModel.loadNextPage() } collectionView.ttg.addPathEffectInfiniteLoad(style: .pulseCircuit, preloadOffset: 160) { await viewModel.loadNextPage() } 这些内置样式不是为了让每个项目都用花哨动画，而是给组件提供一个足够完整的默认能力：普通工具型 App 可以用默认文字 UI，内容型或品牌型 App 可以快速挑一个合适的动画，再用 template 调整颜色、高度、速度、线宽。\nlet template = TTGRefreshPathEffectTemplate( style: .quantumKnot, headerPreferredHeight: 120, footerPreferredHeight: 82, footerContentWidth: 300, colors: [.systemCyan, .systemPurple], animationSpeed: 1.2, lineWidthScale: 1.1 ) tableView.ttg.addPathEffectHeaderRefresh(template: template) { await viewModel.reload() } 完全自定义 View 如果内置样式不够，任意 UIView 都可以作为 header 或 footer，只要实现一个很小的协议。比如自定义 header：\nfinal class CustomHeaderView: UIView, TTGRefreshHeaderContentView { var preferredHeight: CGFloat { 88 } var triggerHeight: CGFloat { 72 } func refreshHeaderDidChange(state: TTGRefreshHeaderState) { switch state { case .idle: break case .pulling(let progress): updatePullProgress(progress) case .willRefresh: showReleaseState() case .refreshing: startLoadingAnimation() case .ending: stopLoadingAnimation() } } func refreshHeaderDidUpdate(progress: CGFloat) { updatePullProgress(progress) } } tableView.ttg.addHeaderRefresh(contentView: CustomHeaderView()) { await viewModel.reload() } 库只规定状态协议，不规定你的 UI 长什么样。这样对宿主页面的侵入就会低很多：页面不需要知道 header 内部动画怎么画，header 也不需要知道业务怎么加载数据。\n架构 内部大致分成几块：\nUIScrollView+TTGRefresh.swift：通过 associated object 安装 header/footer，提供 scrollView.ttg 入口。 TTGRefreshHeader / TTGRefreshFooter：负责 scroll observation、状态变化、触发条件和结束动画。 TTGRefreshActivityCoordinator：协调 header/footer，避免重复任务和交叉触发。 TTGRefreshInsetStore：记录和恢复 inset，处理 contentInset / overlay 模式。 TTGRefreshDefaultHeaderView / TTGRefreshDefaultFooterView：默认文字 UI。 TTGRefreshPathEffectStyle：内置路径动画样式和 template。 这里我刻意把“运行时行为”和“展示内容”拆开。刷新控件最容易变复杂的地方，就是 scroll 状态、inset、动画、业务回调混在一个类里。拆开之后，默认 UI、path effect、自定义 View 都能走同一个状态机。\n总结 TTGRefresh 目前是 0.1.0，但它不是一个只做最小 Demo 的项目。它一开始就希望把 API、状态机、自定义能力和内置动画都放到一个相对完整的结构里。\n我自己最看重的几个点是：API 足够简单、对宿主 UI 低侵入、定制能力足够强、内置样式开箱即用。后面如果继续扩展，也应该围绕这几个原则来做，而不是把它做成一个很重、很难接入的列表框架。\n","date":"2026-07-11T00:00:00Z","image":"/assets/2026/07/11/ttgrefresh-0-1-0/promo_poster.png","permalink":"/2026/07/11/ttgrefresh-0-1-0/","title":"全新开源项目 TTGRefresh：一个低侵入、高定制的刷新组件"},{"content":"前言 距离上一次写博客已经过去 9 年了。回头看了一下最后几篇文章的时间，确实有点恍惚，时光荏苒这句话放在这里还挺合适。\n这几年技术变化很快，尤其随着 AI 时代的来临，查资料、整理脉络、校对文字、补齐上下文的成本都低了不少。以前写一篇文章，很多精力会花在资料整理和表达打磨上；现在这些环节可以被工具分担一部分，反而更应该把项目里的问题、判断和思考沉淀下来。\n所以接下来还是想把博客重新写起来，并且尽量不间断地继续更新。在继续写新的内容之前，先把过去几年写过的文章做一次总结和索引，也算是给这个博客重新接上线。\n之前写文章的时候基本都是遇到一个问题、做了一个控件、踩了一个坑，然后顺手整理出来。时间长了以后，单篇文章之间其实是有脉络的：有些是 iOS 工程结构，有些是 Runtime 和内存管理，有些是 UI 交互和开源控件，还有一些是读书笔记、翻译和服务端相关的小实践。本文就把它们按主题重新整理一下，方便以后自己回头查，也方便刚好搜到某一篇的同学顺着同一类问题继续看下去。\n整体回顾 目前博客里的旧文章主要集中在 2014 到 2017 年，一共 42 篇。现在回头看，这些文章当然带着很明显的时间痕迹：Objective-C、Masonry、CocoaPods、早期 Swift、Runtime、反编译，这些都是当时做 iOS 开发绕不开的话题。\n不过很多问题并没有过时，只是换了技术栈和工具形态。比如代码怎么拆、接口怎么设计、复杂页面怎么优化、遇到奇怪问题怎么验证，这些到现在依然是工程里每天都会遇到的事情。\n大致按年份看：\n2014 年，主要是刚开始写 Blog，以及一个 Android 开源控件。 2015 年，文章最多，覆盖了 iOS 项目结构、Objective-C 基础、UITableView 性能、GCD、Auto Layout、Runtime、Swift 控件等。 2016 年，更多是在总结工程化、模块化、代码组织、API 设计，也有一些更底层的内存管理和消息发送内容。 2017 年，主要集中在 Runtime、反编译、Xcode Bug 分析，以及几个控件和工具库。 如果按阅读路线来分，则可以这样看：\n想看 iOS 工程实践，可以先看项目结构、模块化、代码组织。 想看 Objective-C 底层，可以从 @autoreleasepool、__autoreleasing、objc_msgSend、methodSignatureForSelector: 这几篇开始。 想看 UI 实现，可以看 Auto Layout 系列、UITextView 图文混排、几个 TTG 开源控件。 想查具体问题，就直接按下面的主题索引找。 工程结构、架构与协作 这一类文章主要讨论“代码应该怎么放、怎么拆、怎么协作”。很多观点放到现在不一定是唯一答案，但是问题本身一直存在：项目变大以后，如何让代码还能被理解、修改和复用。\niOS项目的目录结构-原创\n比较早的一篇工程结构整理，重点是从目录层面把业务、工具、资源、第三方代码分清楚。\nEntity和Model的不同-关于代码的数据层\n讨论数据层对象的职责边界，适合和后面的 ViewModel、数据加工、接口返回设计一起看。\n利用NSProxy实现消息转发-模块化的网络接口层设计-原创\n从 NSProxy 和消息转发切入，把网络接口层做成更清晰的调用入口。\n对组件化与模块化的思考与总结\n当时读了不少组件化文章后做的一次整理，重点是“模块”和“组件”的区别，以及如何从业务、技术、界面、数据几条线去拆分。\n总结一些iOS项目中组织代码的方法\n从 CocoaPods 私有库、Carthage、Xcode Group、Category、#pragma mark 等不同粒度整理代码组织方法。\nAPI返回结果设计经验与总结\n虽然分类放在服务端，但本质也是客户端和服务端协作的接口契约问题。\n一次审核被拒的经历-关于iCloud到底应该备份什么数据\n一个偏实践的审核问题，记录了 iCloud 备份数据边界，属于踩坑型文章。\nObjective-C、Runtime 与内存管理 这一组文章是我自己回头看时比较有连续性的部分：一开始是内存和属性行为，后面慢慢过渡到 Runtime、消息转发、反编译和系统实现。\nNSString的Copy与内存分配\n从 NSString、NSMutableString 的 copy 行为入手，看对象拷贝和内存分配。\n@autoreleasepool-内存的分配与释放\n整理 @autoreleasepool 的基本行为，是后面看 __autoreleasing 的基础。\n结合访问Out Parameters出现EXC_BAD_ACCESS的例子，反编译汇编解读__autoreleasing\n通过 Out Parameters 的 crash 例子，继续看 __autoreleasing 和 autorelease pool 的关系。\n翻译-为什么objc_msgSend必须用汇编实现\n一篇翻译文章，但对理解 Objective-C 消息发送、寄存器、调用约定很有帮助。\n用Runtime的手段填充任意NSObject对象的nil属性\n一个用 Runtime 遍历属性并填充默认值的小实践，适合理解属性列表和动态处理。\n解决iOS项目的版本兼容问题-结合宏、Category和Runtime\n用宏、Category、Runtime 做版本兼容处理，属于工程实践和 Runtime 的交叉。\n在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper\n围绕对象销毁阶段执行任务，涉及生命周期、关联对象和封装方式。\n反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash\n一个比较典型的“写 Demo 验证 -\u0026gt; 反编译分析 -\u0026gt; 总结原因”的问题排查过程。\n反编译分析并模拟实现methodSignatureForSelector方法\n从 methodSignatureForSelector: 的行为差异出发，最后尝试模拟实现，适合作为 Runtime 深入阅读的一篇。\n并发、性能与小技巧 这一类文章更偏“项目里马上能用到”的经验。现在很多技术栈已经变化了，但里面的一些判断方式仍然有参考意义，比如提前计算、缓存、减少重复创建、明确线程上下文等。\n提升UITableView性能-复杂页面的优化\n从复杂动态页面入手，总结 Cell 复用、高度缓存、数据预处理、View 缓存等优化点。\nGCD使用经验与技巧浅谈\n对 GCD 常见使用方式做了一次整理，包括队列、同步异步、延迟执行等。\n为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权\n一个更细的问题：如何给 GCD 队列绑定 Objective-C 对象，并正确处理内存管理权。\nBlock类型变量-缓存Http请求与回调\n通过 Block 保存回调，解决重复请求和回调分发问题。\n有关宏定义的经验与技巧-简化代码-增强Log\n记录了一些宏定义用法，重点是让重复代码更少、调试信息更清楚。\nUI、布局与交互实现 这一组基本都是“先有一个效果，再拆解实现”的文章。相比纯理论，我自己更喜欢这种写法：先把画面、交互、约束关系摆出来，再一点点推到代码。\nUITextView编辑时插入自定义表情-简单的图文混编\n记录 UITextView 中插入自定义表情的基础实现。\nUITextView编辑时插入自定义表情-续-自定义表情图片的大小\n继续处理 NSTextAttachment 图片大小的问题，是上一篇的补充。\n有趣的Autolayout示例-Masonry实现\nAuto Layout + Masonry 系列第一篇，用有趣的布局示例解释约束。\n有趣的Autolayout示例2-Masonry实现\nAuto Layout 系列第二篇，继续通过示例练习约束关系。\n有趣的Autolayout示例3-Masonry实现\nAuto Layout 系列第三篇。\n有趣的Autolayout示例4-Masonry实现\nAuto Layout 系列第四篇。\n有趣的Autolayout示例5-Masonry实现\nAuto Layout 系列第五篇，也是这个系列后面的一次补充。\n用QuartzCode快速实现一个收藏动画\n通过 QuartzCode 快速做收藏动画，重点是工具生成动画代码后的理解和使用。\n开源项目与控件实现 这些文章基本都围绕一个具体控件或工具展开：为什么做、最终效果是什么、API 怎么设计、关键实现点在哪里。现在看起来，有些库可能已经偏老了，不过当时做开源项目时对封装、文档、Demo 的训练还是很有价值。\nAndroid开源库-LinkTextView-原创\n早期的 Android 开源控件，用来处理文本中的链接点击。\nSwift写的库-TTGSnackbar\n一个 Swift 写的 Snackbar 控件，重点在交互展示和 API 封装。\nSwift开源项目: TTGEmojiRate的实现\n基于 UIBezierPath、手势和绘制实现 Emoji 评分控件。\n开源项目-TTGTagCollectionView\n标签布局控件，适合看布局计算、数据源和代理 API 的设计。\nSwift开源Mac App - BingWallPaper\n一个 macOS 菜单栏应用，用 Swift 获取 Bing 壁纸并设置桌面背景。\n开源项目-拼图验证控件TTGPuzzleVerify的实现\n拼图验证控件，包含图片裁剪、遮罩、滑动验证等实现点。\n在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper\n既是开源库，也可以归到 Runtime 和对象生命周期相关内容里。\n读书笔记与翻译 这部分主要是阅读《Effective Objective-C》和技术文章后的整理。它们不算完整翻译，更像是把书里的点和自己的理解合在一起做记录。\nEffective-Objective-C-读书笔记-Item-1\nEffective Objective-C 系列笔记开头。\nEffective-Objective-C-读书笔记-Item-2\n继续整理 Objective-C 基础概念。\nEffective-Objective-C-读书笔记-Item-3\n读书笔记第三篇。\nEffective-Objective-C-读书笔记-Item-4-如何正确定义常量\n重点是常量定义方式，也是实际项目里很常见的问题。\nEnum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5\n整理枚举的正确使用方式，包括位掩码等内容。\n翻译-为什么objc_msgSend必须用汇编实现\n这篇同时也属于 Runtime 主题，放在这里是因为它本身是翻译文章。\n服务端、跨端与其它 虽然博客大部分是 iOS，但也有几篇不是纯 iOS 的内容。\nRPC框架Thrift例子-PHP调用C++后端程序\n用一个小例子记录 Thrift、PHP、C++ 后端之间的调用。\nAPI返回结果设计经验与总结\n从接口返回结构出发，讨论 code、message、data 等字段的设计。\nAndroid开源库-LinkTextView-原创\nAndroid 控件文章，也算是早期跨端尝试的一部分。\n第一篇Blog\n真正意义上的第一篇，更多是一个开始的记录。\n按年份索引 如果不按主题，而是按时间看，脉络会更明显一些。\n2014 第一篇Blog Android开源库-LinkTextView-原创 2015 iOS项目的目录结构-原创 Effective-Objective-C-读书笔记-Item-1 Effective-Objective-C-读书笔记-Item-2 Effective-Objective-C-读书笔记-Item-3 利用NSProxy实现消息转发-模块化的网络接口层设计-原创 NSString的Copy与内存分配 提升UITableView性能-复杂页面的优化 Block类型变量-缓存Http请求与回调 一次审核被拒的经历-关于iCloud到底应该备份什么数据 Entity和Model的不同-关于代码的数据层 UITextView编辑时插入自定义表情-简单的图文混编 Effective-Objective-C-读书笔记-Item-4-如何正确定义常量 有关宏定义的经验与技巧-简化代码-增强Log @autoreleasepool-内存的分配与释放 Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5 为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权 GCD使用经验与技巧浅谈 RPC框架Thrift例子-PHP调用C++后端程序 UITextView编辑时插入自定义表情-续-自定义表情图片的大小 有趣的Autolayout示例-Masonry实现 用Runtime的手段填充任意NSObject对象的nil属性 解决iOS项目的版本兼容问题-结合宏、Category和Runtime 有趣的Autolayout示例2-Masonry实现 Swift写的库-TTGSnackbar Swift开源项目: TTGEmojiRate的实现 有趣的Autolayout示例3-Masonry实现 开源项目-TTGTagCollectionView 2016 对组件化与模块化的思考与总结 总结一些iOS项目中组织代码的方法 结合访问Out Parameters出现EXC_BAD_ACCESS的例子，反编译汇编解读__autoreleasing API返回结果设计经验与总结 翻译-为什么objc_msgSend必须用汇编实现 有趣的Autolayout示例4-Masonry实现 Swift开源Mac App - BingWallPaper 开源项目-拼图验证控件TTGPuzzleVerify的实现 2017 用QuartzCode快速实现一个收藏动画 在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper 有趣的Autolayout示例5-Masonry实现 反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash 反编译分析并模拟实现methodSignatureForSelector方法 总结 这次整理之后再看，过去这些文章其实大致围绕几个关键词：工程结构、Objective-C 底层、UI 实现、性能优化、开源封装。\n单篇文章写的时候往往只是解决一个具体问题，但放在一起看，就能看到当时学习和做项目的路径：先解决页面和业务问题，再关注代码组织和模块拆分，最后慢慢深入到 Runtime、内存管理和反编译验证。\n隔了 9 年再重新开始写博客，感觉像是把一个很久没打开的项目重新拉起来。代码可以重新编译，文章也可以继续更新。AI 工具会让整理和表达的成本更低，但真正值得记录的，还是项目里遇到的问题、做判断时的取舍，以及过一段时间后依然能回头看的经验。\n后面新的文章也会沿着这个索引继续补充。这样博客就不只是按时间堆在一起，而是能变成一份可以回头查、也可以顺着阅读的技术笔记了。\n","date":"2026-07-10T00:00:00Z","permalink":"/2026/07/11/myblog-posts-index/","title":"九年之后，重新整理一下过去写过的文章"},{"content":"前言 最近跟同事讨论了有关methodSignatureForSelector:的问题，大概如下：\n一个ProtocolA声明了一个实例方法funcA，然后一个类ClassA声明实现这个ProtocolA，但是并没有实现方法funcA，对ClassA的实例调用methodSignatureForSelector:@selector(funcA)能否返回正确的signature？ 一个类ClassB，在@interface声明了实例方法funcB，但是没有实现funcB，对ClassB的实例调用methodSignatureForSelector:@selector(funcB)，能否返回正确的signature？ 写个Demo验证了下，结果非常有意思，不由得好奇起来，所以深入研究了下methodSignatureForSelector:的实现，然后自己模拟实现出来。\nGithub地址：TTGRemakeMethodSignatureForSelector\n详细的研究过程如下：\n写Demo验证问题 测试类TestClass和Protocol如下：\n@protocol TestProtocol \u0026lt;NSObject\u0026gt; - (void)protocolTestFunc1; @end @interface TestClass : NSObject \u0026lt;TestProtocol\u0026gt; // 不实现Protocol任何方法 - (void)testFunc1; // 不实现 @end @implementation TestClass @end 调用methodSignatureForSelector:获取protocolTestFunc1和testFunc1的methodSignature，protocolTestFunc1可以获取到，但是testFunc1就不行。\n扩展到所有情况 分析整理下可能的情况：\n对于一个类：\n方法没有声明，也没有实现 方法有声明，但是没有实现 方法没有声明，但是有实现 实例方法和类方法都有上面1、2、3的情况 对于一个Protocol：\n方法有声明，但是没有实现 实例方法和类方法 required和optional方法 经过验证，发现：\n对于类，只要@implementation里实现了方法，不管@interface是否有声明，都可以返回methodSignature 对于Protocol声明的方法，不管实例方法还是类方法，不管required还是optional的，都可以返回methodSignature 所以，有必要进一步研究methodSignatureForSelector:的实现。\n从Runtime源码入手 methodSignatureForSelector:是NSObject的方法，所以想知道原理，第一步当然就是翻看Runtime的源码，看看有没有实现，最新的objc4-709源码，实现如下：\n// From objc4-709 NSObject.mm // Replaced by CF (returns an NSMethodSignature) - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { _objc_fatal(\u0026#34;-[NSObject methodSignatureForSelector:] \u0026#34; \u0026#34;not available without CoreFoundation\u0026#34;); } 可见，Runtime源码是没有实现的，要到CoreFoundation里面找。\n找CoreFoundation源码 接着Google CoreFoundation源码，找到https://opensource.apple.com/source/CF/CF-1153.18/，怎么都找不到有关methodSignatureForSelector:的实现，难道要就此打住？\n反编译大法 既然找不到源码，那就只好祭出反编译大法，直接上汇编=。=\n找到CoreFoundation动态库 CoreFoundation是在程序运行时链接上去的动态库，属于系统动态库的一部分，所以首先要找到CoreFoundation动态库的二进制代码文件，命令行：\nsudo -i find / -name CoreFoundation.framework 搜到的结果很多，以iPhone模拟器的为例：\n/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CoreFoundation.framework 打开CoreFoundation.framework文件夹，CoreFoundation文件就是要反编译的文件。\nHopper Disassembler反编译 用Hopper Disassembler打开CoreFoundation文件，在Labels搜索methodSignatureForSelector:，就能找到对应的汇编实现代码，如下：\nHopper翻译的代码如下：\nvoid * +[NSObject methodSignatureForSelector:](void * self, void * _cmd, void * arg2) { rdi = self; rbx = arg2; if ((rbx != 0x0) \u0026amp;\u0026amp; (___methodDescriptionForSelector(object_getClass(rdi), rbx) != 0x0)) { rax = [NSMethodSignature signatureWithObjCTypes:rdx]; } else { rax = 0x0; } return rax; } 可发现，关键在于___methodDescriptionForSelector:方法。\n___methodDescriptionForSelector:方法的实现 进一步看___methodDescriptionForSelector:的实现。\n汇编代码、Hopper翻译的代码都很长，循环也不见了，全是goto：\n经过反复校对整理（排除Hopper的错误=。=），可以整理出主要的流程如下图所示：\n根据流程，可以得出，___methodDescriptionForSelector:方法：\n用class_copyProtocolList、protocol_getMethodDescription方法，检查是否有对应的selector，不管是否实现 用class_getInstanceMethod检查selector是否有implementation，注意，是implementation，跟是否声明了没有关系 所以，符合前面的结论。\n模拟实现 流程都有了，不如更进一步，自己实现一个methodSignatureForSelector:方法出来～\n模拟实现___methodDescriptionForSelector: 按照流程，实现代码如下：\n// 模拟实现___methodDescriptionForSelector方法： struct objc_method_description ttg_MethodDescription(Class class, SEL sel) { // 结果 struct objc_method_description description = (struct objc_method_description){NULL, NULL}; Class currentClass = class; while (currentClass \u0026amp;\u0026amp; description.name == NULL) { // 获取Protocol列表 unsigned int count = 0; __unsafe_unretained Protocol **protocols = class_copyProtocolList(currentClass, \u0026amp;count); // 遍历所有Protocol for (unsigned int i = 0; i \u0026lt; count; i++) { // Required 方法 description = protocol_getMethodDescription(protocols[i], sel, YES, class_isMetaClass(currentClass) ^ 1); if (description.name != NULL) { break; } // Optional 方法 description = protocol_getMethodDescription(protocols[i], sel, NO, class_isMetaClass(currentClass) ^ 1); if (description.name != NULL) { break; } } // 释放 free(protocols); // 找到、返回 if (description.name != NULL) { return description; } // 获取父类，继续 currentClass = class_getSuperclass(currentClass); } // 获取实例方法 Method method = class_getInstanceMethod(class, sel); if (method) { // 找到实例方法 return *method_getDescription(method); } else { // 返回空 return (struct objc_method_description){NULL, NULL}; } } 用description记录迭代中的结果，只要找到就返回 用currentClass记录每次迭代的类class Protocol的@required和@optional方法都要检查 注意释放Protocol数组 最后才用class_getInstanceMethod获取类本身的实现 模拟实现methodSignatureForSelector: 用Category给NSObject加两个方法：\n@interface NSObject (TTGRemakeMethodSignatureForSelector) - (NSMethodSignature *)ttg_methodSignatureForSelector:(SEL)sel; + (NSMethodSignature *)ttg_methodSignatureForSelector:(SEL)sel; @end 实现基本一样：\n@implementation NSObject (TTGRemakeMethodSignatureForSelector) - (NSMethodSignature *)ttg_methodSignatureForSelector:(SEL)sel { struct objc_method_description description = ttg_MethodDescription([self class], sel); // 注意！是获取实例的class if (sel \u0026amp;\u0026amp; description.types != NULL) { return [NSMethodSignature signatureWithObjCTypes:description.types]; } else { return nil; } } + (NSMethodSignature *)ttg_methodSignatureForSelector:(SEL)sel { struct objc_method_description description = ttg_MethodDescription(object_getClass(self), sel); // 必须用object_getClass获取metaClass if (sel \u0026amp;\u0026amp; description.types != NULL) { return [NSMethodSignature signatureWithObjCTypes:description.types]; } else { return nil; } } @end 注意要获取正确的”class”：\n对于对象实例，从[obj class]、object_getClass(obj)结果一致，都是对象的类 对于类，要获取元类，也就是metaClass，只能用object_getClass(class) 验证模拟实现 验证自己模拟实现的methodSignatureForSelector:和系统的是否一致：\n// 测试equal相等 NSMethodSignature *signature1 = [test methodSignatureForSelector:@selector(testFunc1)]; NSMethodSignature *signature2 = [test ttg_methodSignatureForSelector:@selector(testFunc1)]; NSLog(@\u0026#34;signature1 == signature2: %d\u0026#34;, [signature1 isEqual:signature2]); // Invoke实例方法 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[test ttg_methodSignatureForSelector:@selector(testFunc1)]]; invocation.target = test; invocation.selector = @selector(testFunc1); [invocation invoke]; // Invoke类方法 invocation = [NSInvocation invocationWithMethodSignature:[TestClass ttg_methodSignatureForSelector:@selector(staticTestFunc1)]]; invocation.target = [TestClass class]; invocation.selector = @selector(staticTestFunc1); [invocation invoke]; 结果跟系统实现的行为一致，更多Case见Github的Demo。\n总结 从两个小问题出发，到最后通过反编译、模拟实现，终于搞清楚了methodSignatureForSelector:的原理、流程、各种隐藏特性！哈哈～～过瘾～～\n","date":"2017-04-08T00:00:00Z","image":"/assets/2017/04/08/diy-methodSignatureForSelector/blog_RemakeMethodSignatureForSelector_1.jpg","permalink":"/2017/04/08/diy-methodSignatureForSelector/","title":"反编译分析并模拟实现methodSignatureForSelector方法"},{"content":"更新 在最新的Xcode 9 Beta版本中，此Bug已被苹果修复：Bug ID 30530580: Crash on “Release” Build Configuration scheme setting for Out Parameters code 二级指针 二级指针，也叫指针的指针，或者Out Parameters，可以用来改变一个指针的地址值，由于在Objective-C里面方法、函数不支持返回多个值，所以经常用二级指针实现这个功能，比如NSFileManager的- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error方法，就可以让方法在内部创建error后传出。\n问题 最近在Debug代码的时候，注释掉了一个带有二级指针参数的方法内部所有代码，然后在Release环境下安装运行，结果居然Crash了，猛然想起好像以前同事也遇到过，仔细检查了下，感觉代码是没有问题的，所以继续深究，新建了一个空的工程，重现了这个EXC_BAD_ACCESS的Crash，代码非常简单，如下：\n// main.m #import \u0026lt;Foundation/Foundation.h\u0026gt; #import \u0026lt;CoreFoundation/CoreFoundation.h\u0026gt; @interface TestClass : NSObject @end @implementation TestClass + (void)runTest { NSMutableArray *array = [NSMutableArray new]; [TestClass testFunc:\u0026amp;array]; [TestClass testFunc:\u0026amp;array]; NSLog(@\u0026#34;array: %@\u0026#34;, array); } + (void)testFunc:(NSMutableArray **)array { } @end int main(int argc, const char * argv[]) { @autoreleasepool { [TestClass runTest]; } return 0; } 经测试，在Xcode 8.0、以及最新的8.2.1稳定版下，Release环境中，都会Crash，放到iOS工程里面，模拟器、真机也会。但是，在Xcode7下就没有问题。\n分析 源码分析 首先，从代码本身上来看，是没有问题的，带有二级指针的方法testFunc:是空的方法，虽然传进来了NSMutableArray **array，但是没有对其赋值。\nrunTest方法也只是创建了一个NSMutableArray对象，然后取了指针的指针，调用了两次testFunc:，然后打印，按理来说是没有任何副作用的。\n所以，直接从代码上看，发现不了问题的原因。\nclang -rewrite-objc分析 直接看代码没有用，那就用clang重写出C++代码分析，命令行对main.m执行clang -rewrite-objc main.m，得到C++重写后的代码，在文件的最后，就能找到testFunc:和runTest的C++代码，如下：\n// runTest的C++代码 static void _C_TestClass_runTest(Class self, SEL _cmd) { NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass(\u0026#34;NSMutableArray\u0026#34;), sel_registerName(\u0026#34;new\u0026#34;)); ((void (*)(id, SEL, NSMutableArray **))(void *)objc_msgSend)((id)objc_getClass(\u0026#34;TestClass\u0026#34;), sel_registerName(\u0026#34;testFunc:\u0026#34;), \u0026amp;array); ((void (*)(id, SEL, NSMutableArray **))(void *)objc_msgSend)((id)objc_getClass(\u0026#34;TestClass\u0026#34;), sel_registerName(\u0026#34;testFunc:\u0026#34;), \u0026amp;array); NSLog((NSString *)\u0026amp;__NSConstantStringImpl__var_folders__5_6lqsnn195jj6pw61j66npcnm0000gn_T_main_5a7341_mi_0, array); } // testFunc:的C++代码 static void _C_TestClass_testFunc_(Class self, SEL _cmd, NSMutableArray **array) { } 从C++代码来看，也没有可能会导致EXC_BAD_ACCESS问题的释放、dealloc之类的代码，继续！\nHopper Disassembler反编译分析 重写出C++代码无法解决，就只能让Hopper出马了，毕竟“汇编面前，没有秘密”=。=，以x86_64架构二进制为例。\n先来看testFunc:的：\n跟预想的一致，没有任何“有效代码”。\n再看runTest方法：\n为了方便分析，把反编译出来的汇编代码分了段：\n调用NSMutableArray new创建array变量，保存在r15寄存器上 第一次调用testFunc: 先retain，然后release，rbx保存array，r15释放 第二次调用testFunc: 先release，再retain，即释放了rbx，然后又retain，这里明显有问题 调用NSlog打印 最后release释放array变量 为了进一步验证，用Hopper的反汇编生成Objective-C代码功能进行验证，生成的代码如下：\n// Xcode8 Release环境编译的x86_64二进制反编译后生成的代码 void +[TestClass runTest](void * self, void * _cmd) { // 创建 r15 = [NSMutableArray new]; // 第一次testFunc: [TestClass testFunc:r15]; // retain然后release rbx = [r15 retain]; [r15 release]; // 第二次testFunc: [TestClass testFunc:rbx]; // 先release !!! [rbx release]; // 然后retain rbx = [rbx retain]; // NSLog打印 NSLog(@\u0026#34;array: %@\u0026#34;, rbx); // 释放 [rbx release]; return; } release和retain的顺序错误 Hopper生成的代码跟汇编分析基本一致，至此，可以看出，唯一“不正常”的代码就是第5段的**“先release，再retain”**，这会导致array变量提前释放，以至于后面再次retain无效，NSLog的时候，出现访问已被释放的变量，于是出现EXC_BAD_ACCESS崩溃。\n对比Xcode7 现在已经基本可以确定问题了，但是为什么Xcode7就不会Crash？反编译一探究竟。\n相同的代码，Xcode7 Release环境编译出可执行二进制文件，反编译后，用Hopper生成Objective-C代码，然后跟Xcode8的做对比，如下：\n可以看出，Xcode8编译后，release和retain的顺序错了，导致了array变量提前被释放，继而EXC_BAD_ACCESS错误。而Xcode7编译的一切正常。\n问题原因小结 现在可以大胆猜测，EXC_BAD_ACCESS的原因就是Xcode8的编译错误。 在编译上面的代码时，Xcode8不能正确的处理array变量作为二级指针传入testFunc空函数后的release、retain顺序，导致array被提前释放，最终产生EXC_BAD_ACCESS错误。\n目前已经给Xcode提了Bug report，等待苹果的回复～\n所有的代码、Xcode7和8编译出的二进制可执行文件、Hopper的文件，打包放在了Github上：https://github.com/zekunyan/OutParameterPointerCrashOnXcode8\n总结 汇编面前，没有秘密！=。=\n","date":"2017-03-14T00:00:00Z","image":"/assets/2017/03/14/xcode8-crash-on-out-parameter/xcode8_crash_on_out_parameter_1.jpg","permalink":"/2017/03/14/xcode8-crash-on-out-parameter/","title":"反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash"},{"content":"前言 第五篇来了！ 还是3个小例子，仍然是主要部分用Masonry手写代码实现，其它的约束在storyboard里面直接拖拽搭建。三个例子分别是：\nUITableViewCell中多个变高的Label StairView自定义View的intrinsicContentSize，并支持在UITableViewCell中自动计算高度 给同一个属性添加多重约束，实现复杂关系 前四篇：\n第一篇：有趣的Autolayout示例-Masonry实现 第二篇：有趣的Autolayout示例2-Masonry实现 第三篇：有趣的Autolayout示例3-Masonry实现 第四篇：有趣的Autolayout示例4-Masonry实现 Github地址： https://github.com/zekunyan/AutolayoutExampleWithMasonry\nCase1: UITableViewCell中多个变高的Label 实现功能 功能就是Cell整体变高，内部水平三个Label，宽度一样，高度由内容决定。\n在本例子中，Label的宽度定为80，避免多行Label的preferredMaxLayoutWidth的设置干扰。\n原理 原理一句话就是：Cell的高度由三个Label中最高的得出。\n但是，实际设置Autolayout的时候，不存在什么cell.height = MAX(labels.height)这样的关系。\n所以要转换一下，改为**ANY(label.bottom) \u0026lt;= cell.bottom，即每个Label的底部bottom都是在cell的底部bottom上面的**，换算到坐标系里面，就是在Y轴上，label.bottom \u0026lt;= cell.bottom。\n所以，cell的约束如下图所示：\n代码细节 对应的代码如下：\n- (void)setupLayout { // label1的约束 [_label1 mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(@80); // 宽度80 make.top.equalTo(self.contentView.mas_topMargin); // 上 make.left.equalTo(self.contentView.mas_leftMargin); // 左 // label.bottom \u0026lt;= cell.contentView.bottom，注意是在Y轴上对比 make.bottom.lessThanOrEqualTo(self.contentView.mas_bottomMargin); }]; // label2的约束 // 类似... // label3的约束 // 类似... } Case2: StairView自定义View实现intrinsicContentSize 实现功能 实现一个自定义布局的View - StairView，阶梯布局View。 StairView支持设置Label成阶梯状从上到下、从左到右分布，如下图：\n同时，StairView要支持Autolayout，支持大小由内容决定，可以在Cell里面用，并自动计算高度。\n接下来，看看如何实现这个StairView。\nintrinsicContentSize是什么 如果说到Autolayout的内容决定大小，就会遇到intrinsicContentSize方法，这个方法是什么呢？\nintrinsic单词的意思：\nadj.固有的，内在的，本质的； [解剖]体内的； 本征； 先天性\n而intrinsicContentSize方法的官方解释是：\nReturns the natural size for the receiving view, considering only properties of the view itself.\n所以，UIView的intrinsicContentSize，就是获取这个View的固有的，内在的，本质的的内容大小。\n在看Discussion部分：\nCustom views typically have content that they display of which the layout system is unaware. Overriding this method allows a custom view to communicate to the layout system what size it would like to be based on its content. This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.\n比较长，简单说，就是Autolayout布局系统是不知道自定义View的内容大小的，所以自定义View可以重写这个方法，返回自己的内容大小，交由Autolayout布局系统去计算布局。并且，这个大小是不能依赖外部的大小的。\n所以，如果我们的View的大小是固定的时候，最简单，直接返回即可：\n- (CGSize)intrinsicContentSize { return CGSizeMake(100, 100); // 固定100x100大小 } 如果大小需要计算，就要先计算，然后返回：\n- (CGSize)intrinsicContentSize { // 先计算内容大小，可以按情况缓存，减少计算量 CGSize contentSize = [self calculateContentSize]; // 返回 return contentSize; } 有了intrinsicContentSize的返回大小，Autolayout的布局系统才能正确的布局。\nStairView的实现 清楚了intrinsicContentSize的作用以后，就可以着手实现StairView了。\nAPI设计 为了尽量简单，StairView的方法只有一个：\n/** * 设置阶梯title * @param titles 标题数组 */ - (void)setStairTitles:(NSArray \u0026lt;NSString *\u0026gt; *)titles; 直接传入字符串数组即可。\n内部属性 StairView内部由UILabel组成实现，所以需要一个数组保存所有的Label，还要一个CGSize类型的变量，保存当前的内容大小：\n@interface StairView () @property (nonatomic, strong) NSMutableArray \u0026lt;UILabel *\u0026gt; *itemViews; // Label数组 @property (nonatomic, assign) CGSize contentSize; // 内容大小，随时更新 @end 设置阶梯标题: setStairTitles:的实现 接着就是实现setStairTitles:方法。 基本逻辑就是: 删除旧的Label-\u0026gt;循环创建新的Label-\u0026gt;重新布局：\n- (void)setStairTitles:(NSArray\u0026lt;NSString *\u0026gt; *)titles { // 批量从superView删除旧的Label [_itemViews makeObjectsPerformSelector:@selector(removeFromSuperview)]; // 重新创建 _itemViews = [NSMutableArray new]; // 循环创建新的Label for (NSString *title in titles) { UILabel *label = [UILabel new]; // 设置样式 label.text = title; label.font = [UIFont systemFontOfSize:12]; label.layer.borderColor = [UIColor lightGrayColor].CGColor; label.layer.borderWidth = 1.0f / [UIScreen mainScreen].scale; label.numberOfLines = 0; [self addSubview:label]; // 添加Label [_itemViews addObject:label]; } // 重新布局 _contentSize = CGSizeZero; // 重点！ [self updateStairLayout]; } 内部阶梯Label的布局+intrinsicContentSize的计算: updateStairLayout的实现 从上面的设置代码可看出，重点是对内部Label布局的updateStairLayout方法，按照前面所说的功能要求，基本的原理就是: 根据当前阶梯数量计算每个Label的平均width-\u0026gt;遍历所有Label计算当前width下的高度-\u0026gt;设置每个Label的frame-\u0026gt;更新contentSize\n- (void)updateStairLayout { if (_itemViews.count == 0) { // 没有设置内容，内容大小为零 _contentSize = CGSizeZero; return; } // 计算每个Label的平均宽度 CGFloat eachLabelWidth = CGRectGetWidth(self.bounds) / (CGFloat)_itemViews.count; CGFloat lastY = 0; CGFloat lastX = 0; for (UILabel *label in _itemViews) { // 根据内容、当前width计算高度 label.preferredMaxLayoutWidth = eachLabelWidth; CGFloat height = [label sizeThatFits:CGSizeMake(eachLabelWidth, 0)].height; // 设置frame CGRect frame = CGRectMake(lastX, lastY, eachLabelWidth, height); label.frame = frame; // 更新下一轮 lastX += eachLabelWidth; lastY += height; } // 更新contentSize CGSize newContentSize = CGSizeMake(CGRectGetWidth(self.bounds), lastY); // 判断内容大小是否有变化 if (!CGSizeEqualToSize(newContentSize, _contentSize)) { // 更新contentSize _contentSize = newContentSize; // 通知外部IntrinsicContentSize失效 [self invalidateIntrinsicContentSize]; } } 要先判断设置的阶梯title数量是否为零，否则计算平均宽度时会导致除数为零的Crash 每个Label的高度，根据当前的平均宽度，用sizeThatFits:计算得出 最后更新_contentSize的值 重点！[self invalidateIntrinsicContentSize]通知外界，StairView的内容大小发生变化 计算内部Label布局的原理还是很简单的，重点就是最后的更新_contentSize和调用[self invalidateIntrinsicContentSize]。\n通知外界内容大小发生变化-invalidateIntrinsicContentSize方法 先看看官方文档的解释：\nCall this when something changes in your custom view that invalidates its intrinsic content size. This allows the constraint-based layout system to take the new intrinsic content size into account in its next layout pass.\n意思就是，当自定义View的内容大小发生变化时，调用此方法通知布局系统，在下一次布局更新中刷新。\n所以，对照上面的代码:\n// 判断内容大小是否有变化 if (!CGSizeEqualToSize(newContentSize, _contentSize)) { // 更新contentSize _contentSize = newContentSize; // 通知外部IntrinsicContentSize失效 [self invalidateIntrinsicContentSize]; } 就很明显了，就是按照文档的说明做的。\n关键：何时调用updateStairLayout更新布局、刷新contentSize 有了updateStairLayout方法来布局、计算contentSize，也用了invalidateIntrinsicContentSize通知系统刷新，那么剩下来的关键就是何时何地调用更新布局、contentSize\n在layoutSubviews的时候 这里很明显，layoutSubviews就是刷新自定义view的地方，系统会在布局的过程中，多次调用，如frame改变。\n在intrinsicContentSize的时候 这里比较容易忽视，有时候，系统调用intrinsicContentSize方法的时候，可能在自定义View的布局之前，所以这个时候要“强制”刷新布局、计算出当前状态下正确的contentSize\n所以，有两处地方要加上：\n- (CGSize)intrinsicContentSize { [self updateStairLayout]; return _contentSize; } - (void)layoutSubviews { [super layoutSubviews]; [self updateStairLayout]; } 自定义View小结 StairView的原理、代码都讲完了，算是一个典型的自定义的、根据内容决定高度的View案例，主要要注意的就是更新布局、contentSize的时机，以及及时invalidateIntrinsicContentSize通知布局系统更新\nCase3: 给同一个属性添加多重约束，实现复杂关系 这个Case主要是为了纠正一个可能的误区：Autolayout里面，同一个属性，是可以加上多重约束的，或者说，同一个View的同一个属性，可以参与到多个约束里面的。\n先看看Case的功能：\n注意到，右边绿色label的左边，不仅要大于左边蓝色label的右边，还要大于整体宽度的1/3，所以，是要同时满足多条约束条件的。\n区分观念：Frame布局和Autolayout的不同 按照Frame布局\n按照传统的用Frame布局的方式，label的左边，其实就是label.frame.origin.x这个属性，所以，要左边满足条件的话，首先想到的可能就是：\n// 满足大于蓝色label的右边 label.frame.origin.x \u0026gt; blueLabel1.frame.origin.x + blueLabel1.frame.size.width; label.frame.origin.x \u0026gt; blueLabel2.frame.origin.x + blueLabel2.frame.size.width; label.frame.origin.x \u0026gt; blueLabel3.frame.origin.x + blueLabel3.frame.size.width; // 满足大于整体的1/3 label.frame.origin.x \u0026gt; (superView.frame.origin.x + superView.frame.size.width) / 3 这，乍一看，就是不停的在layoutSubView的时候做判断、处理。\n按照Autolayout布局\n而Autolayout是什么，是约束，是一种布局关系，只是对具体View的相关属性的一种关系设置，我们设置了约束，系统就会在需要的时候，按照约束，计算出每个View的frame，自动更新。\n所以，同一个属性，完全是可以加多个约束条件的。\n代码细节 一旦没有了一个属性一个约束的观念限制，这个Case的功能就很好实现了，关键代码如下：\n// 创建绿色label _greenLabel = [UILabel new]; [_containerView addSubview:_greenLabel]; // 设置内容抗压优先级最高 [_greenLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; [_greenLabel mas_makeConstraints:^(MASConstraintMaker *make) { // 垂直居中 make.centerY.equalTo(_containerView); // 右边不超出 make.right.lessThanOrEqualTo(_containerView); // 左边大于等于父View宽度的1/3，注意不是width属性 make.left.greaterThanOrEqualTo(_containerView.mas_right).multipliedBy((CGFloat)(1.0f / 3.0f)); // 循环，左边大于所有左边label的右边+8 for (UILabel *label in _leftLabels) { make.left.greaterThanOrEqualTo(label.mas_right).offset(8); } }]; 只要约束没有冲突，我们设置的约束，就可以在各种情况下，按照需要生效。 对于这种内容动态变化，View之间关系复杂的情况，用Autolayout就非常有效了。\n最后 这个系列居然已经出到了第五篇了，自己都觉得不可思议=。= 所有的例子都是日常工作中遇到、想到的，觉得算是一个点，就把它记录了下来，以后会一直坚持下去～\n","date":"2017-03-12T00:00:00Z","image":"/assets/2017/03/12/autolayout-example-with-masonry5/blog_autolayout_example_with_masonry5_1.png","permalink":"/2017/03/12/autolayout-example-with-masonry5/","title":"有趣的Autolayout示例5-Masonry实现"},{"content":"前言 最近更新了下以前写的TTGDeallocTaskHelper库，功能非常简单，就是**在不改变原有代码的情况下，不用Runtime的Method Swizzling给任意对象添加任务Block，在对象dealloc的后期执行Block。**使用时：\n// 给object对象添加dealloc后期要执行的任务Block [object ttg_addDeallocTask:^(__unsafe_unretained id target, NSUInteger identifier) { // 执行任务 }]; Github地址：https://github.com/zekunyan/TTGDeallocTaskHelper\n一张图说明原理：\n库的详细原理、限制、注意事项，为什么说“后期”执行，将在下文给出。\n使用案例 先给出几个使用案例。\n删除Notification监听 比如你给一个第三方类对象添加了Notification通知，要在对象release后删除Notification的监听，就可以：\n[object ttg_addDeallocTask:^(__unsafe_unretained SomeClass *object, NSUInteger identifier) { // 删除Notification监听 [[NSNotificationCenter defaultCenter] removeObserver:object]; }]; 快速Debug内存泄漏 有时你想快速验证一下一个类有没有释放，存不存在内存泄漏，比如一个ViewController返回后，就可以这么做：\n[someViewController ttg_addDeallocTask:^(__unsafe_unretained UIViewController *controller, NSUInteger identifier) { NSLog(@\u0026#34;我正常释放了！哈哈哈！\u0026#34;); }]; 事件打点 业务开发中，想监听某个事件什么时候销毁，就可以：\n[someEvent ttg_addDeallocTask:^(__unsafe_unretained SomeEvent *event, NSUInteger identifier) { // 打点。。。 }]; 是不是很方便！没有对原有的代码做任何侵入。 接下来看看实现原理。\n原理 在对象dealloc的时候做操作的四种办法 在对象dealloc的时候做操作，有好几种方法：\n继承：这个应该是最“正常”的方法了，继承对应的类，在子类中实现dealloc方法，执行相关的任务。缺点是要修改用到类的地方为自己定义的子类 Method Swizzling：方法交换，直接自己定义新的dealloc方法实现，实现任务的调用。但是坏处是所有的对象都会被替换，侵入性太大 ISA Swizzling：动态的生成对象的子类，在子类继承方法，实现调用任务，然后设置对象的isa指针，使其指向子类。这种方法的好处是只会影响到单个对象，但是系统KVO也是用这个方法，重复设置isa的话，会有问题。 利用Associated Object：Associated Object会在对象释放时自动释放，所以可以用来“挂载”要执行的任务。 对比了上面的四种方法后，选定用Associated Object的方式实现TTGDeallocTaskHelper。\n用Associated Object挂载任务 总的思路是：\n给对象添加Associated Object 在Associated Object里面保存要执行的任务Block 实现Associated Object的dealloc方法，在Associated Object释放的时候，批量执行之前添加的Block 图示如下：\nAssociated Object - TaskModel 属性\n上图中的TaskModel，就是库代码里面的TTGDeallocTaskModel类，包含三个属性：\n@property (nonatomic, assign) pthread_mutex_t lock; @property (nonatomic, strong) NSMutableDictionary *tasksDict; @property (nonatomic, unsafe_unretained) id target; tasksDict保存要执行的任务Block lock锁用来保证多线程下操作tasksDict的安全性 target指向的是原对象，是unsafe_unretained的，保证始终不为nil，所以也建议不要在任务Block里面调用target的方法 添加任务Block TTGDeallocTaskModel的tasksDict就是用来保存要执行的任务Block的，为了能在添加了以后删除，所以要以Key-Value的形式保存。 Key的类型是NSUInteger，全局自增，保证唯一。\nOSAtomicIncrement64实现线程安全的自增整型Key-identifier 为了保证自增的Key的线程安全，同时平衡性能，所以此处用OSAtomicIncrement64实现原子整型数自增：\n// 全局静态变量 static volatile int64_t globalIdentifier = 0; // 自增+1，生成新的Key NSUInteger newIdentifier = (NSUInteger)OSAtomicIncrement64(\u0026amp;globalIdentifier); pthread_mutex_t保证操作字典的线程安全 为了保证NSMutableDictionary的多线程操作的安全，用pthread_mutex_t对添加Block的操作加锁：\npthread_mutex_lock(\u0026amp;_lock); [_tasksDict setObject:[taskBlock copy] forKey:newIdentifierNumber]; pthread_mutex_unlock(\u0026amp;_lock); 删除任务Block 删除任务有两种，分别是删除一个identifier key对应的Block，和删除所有Block，同样，都要加锁保证线程安全：\n// 加锁 pthread_mutex_lock(\u0026amp;_lock); // 删除某个identifier对应的Block [_tasksDict removeObjectForKey:identifierNumber]; // 或删除所有 [_tasksDict removeAllObjects]; // 解除锁 pthread_mutex_unlock(\u0026amp;_lock); dealloc执行Block 最终在TaskModel被释放的时候，要批量执行所有添加的Block：\n- (void)dealloc { // 批量执行Block [_tasksDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *identifier, TTGDeallocTaskBlock block, BOOL * _Nonnull stop) { block(_target, identifier.unsignedIntegerValue); }]; // 销毁锁 pthread_mutex_destroy(\u0026amp;_lock); } 给NSObject添加Category 上面是TaskModel的部分，接下来是NSObject的。\n给NSObject添加Categor NSObject (TTGDeallocTaskHelper)，保存TTGDeallocTaskModel，封装调用。\n封装-添加任务Block Associated Object给对象添加TaskModel属性\n用objc_setAssociatedObject实现给任意对象添加对应的TaskModel对象。\n@synchronized保证多线程下TaskModel的唯一性\n首次添加任务Block，会创建TaskModel，为了保证多线程下的唯一性，所以创建的部分要用@synchronized保护起来。\n所以，添加任务Block的代码如下：\n- (NSUInteger)ttg_addDeallocTask:(TTGDeallocTaskBlock)taskBlock { if (!taskBlock) { // block为空 return TTGDeallocTaskIllegalIdentifier; } TTGDeallocTaskModel *model = nil; // 线程安全 @synchronized (self) { model = objc_getAssociatedObject(self, \u0026amp;TTGDeallocTaskModelKey); if (!model) { // 创建 model = [[TTGDeallocTaskModel alloc] initWithTarget:self]; // 保存 objc_setAssociatedObject(self, \u0026amp;TTGDeallocTaskModelKey, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } // 添加新的Block NSUInteger newIdentifier = [model addTask:taskBlock]; return newIdentifier; } 封装-删除Block 直接获取当前对象的TaskModel对象，调用相应的方法即可，不再贴代码。\n限制与注意事项 原理和实现说完了，接下来是库的限制与注意事项。\nNSObject对象的dealloc过程 先来看看Associated Object在dealloc的什么时候释放。\nNSObject对象dealloc的具体细节，sunnyxx在他的《ARC下dealloc过程及.cxx_destruct的探究》已经说的很清楚了。简单来说，就是对象dealloc的时候，最终会调用到objc_destructInstance方法上：\n// 源码：objc4-706 objc-class.mm /*********************************************************************** * objc_destructInstance * Destroys an instance without freeing memory. * Calls C++ destructors. * Removes associative references. * Returns `obj`. Does nothing if `obj` is nil. * CoreFoundation and other clients do call this under GC. **********************************************************************/ void *objc_destructInstance(id obj) { if (obj) { Class isa = obj-\u0026gt;getIsa(); if (isa-\u0026gt;hasCxxDtor()) { // 1. 释放property object_cxxDestruct(obj); } if (isa-\u0026gt;instancesHaveAssociatedObjects()) { // 2. 删除对象的associated objects _object_remove_assocations(obj); } // 3. 设置所有指向此对象的weak指针为nil objc_clear_deallocating(obj); } return obj; } 主要是3步：\nobject_cxxDestruct释放对象的ivar、property _object_remove_assocations释放对象的Associated Object objc_clear_deallocating重置所有指向对象的weak指针为空 不建议在Block里面调用target原对象的方法 从dealloc的流程可知，Associated Object是在第二步被释放的，这个时候，原对象的属性已被release，并且，Associated Object对象自己的dealloc不一定在什么时候会被调用（比如加到了autorelease pool里面），所以，不建议在任务Block里面调用target原对象的方法。\n最后 什么时候Associated Object的释放能在对象属性release之前就好了=。=\n","date":"2017-03-11T00:00:00Z","image":"/assets/2017/03/11/TTGDeallocTaskHelper/TTGDeallocTaskHelper_1.jpg","permalink":"/2017/03/11/TTGDeallocTaskHelper/","title":"在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper"},{"content":"前言 “工欲善其事,必先利其器”，iOS的Core Animation非常强大，可以做出非常炫酷的动画，但是实际写起来还是比较麻烦，代码不直观，需要反复的改参数、Build、运行，所以诞生了各种动画制作工具，本文就以其中比较有名的QuartzCode为例，快速实现一个收藏功能的星星动画，最终的效果如下：\nGitHub地址：https://github.com/zekunyan/QuartzCodeExample-StarAimation\nQuartzCode简介 QuartzCode是一款专门用来创作iOS/macOS动画的软件，基本上把整个Core Animation框架的内容都搬到了软件里面，比如各种Layer、Mask、可以用来动画的属性，基本上跟代码是一致的，完全可以当成**“可视化动画编程”工具，而且制作以后可以导出代码+资源文件**，直接就可以在代码里面调用，方便自定义修改。\n以QuartzCode 1.54.0为例，主体界面如下：\n简单讲下：\nA: Layers，用于动画的Layer在这里显示，先后顺序、层级和StoryBoard比较像 B: Inspector，当前选中元素，如Layer的属性编辑，基本和对应CALayer类的属性一一对应 C: Animations，动画组，可以有多个动画组，每个相互独立 D: Timeline，动画时间轴，每个Layer的动画都会在时间轴上标出 E: Keyframe inspector，动画关键帧的属性编辑，也是跟Core Animation的CAAnimation类以及子类的属性一致 F: 画布，Layer的编辑区 总体上来看，QuartzCode的界面跟XIB、StoryBoard的风格非常类似，还是很容易上手的，更多的教程看官网：http://www.quartzcodeapp.com/\n整理思路 首先整理下思路：\n总体上可以分成两个部分，一个是动画部分，用QuartzCode实现可视化编辑创建；另一个是功能、代码API设置部分，需要在QuartzCode最后导出的代码基础上，添加、修改，封装成简单的控件库。\nQuartzCode实现动画 先来看看动画的实现，本节只把主要步骤流程列出来，详细的参数、设置，参考GitHub上的QuartzCode文件。\n显示星星动画 显示星星的动画由三个部分组成，分别是实心的星星从小到大出现，空心的从大到小并透明度减小消失，最后星星四周闪动圆点、短线。\n实心星星从小到大出现\n拖入图片素材到中间的画布上，软件会自动创建一个layer，可以通过Inspector调整大小、位置 对Layers里面自动生成的layer重命名 点击Animations的“+”按钮，添加一个动画组，命名为show 选中当前的layer，点击Timeline下对应layer的“+”按钮，添加一个Bounds帧动画 Timeline里面的每一行对应帧动画的时间轴，可以在轴上双击、右键添加关键帧，这里默认就好，分别选中Bounds帧动画时间轴两端的关键点，在右边的Keyframe inspector里面设置Size值，起始是（0，0），结束时大小为原图大小 空心星星从大到小、透明度减小消失\n基本和上一步一样，可以先隐藏实心的layer以便于观察。\n添加空心的星星图片素材 命名layer 给空心的layer添加Bounds和Opacity两种动画，分别修改帧动画的起始、结束值 拖动时间轴查看效果 四周闪动短线、圆点\n短线、圆点是沿着星星的四周重复排列的，所以这里可以用CAReplicatorLayer来实现，对应到QuartzCode里面就是工具栏的”Replicator”类型的Layer。CAReplicatorLayer可以根据一定的规则重复的绘制内部的子Layer，在这里只需要按照旋转角度变化就好。\n点击”Replicator”，在中间的画布上拖动出一个layer，然后选择”Shape”里面的圆角矩形，在星星的一角拖动画出一个小的圆角矩形，保证star_layer在replicator layer的内部 设置replicator的属性，实现循环重复内部的layer，这里设置”instance count”为5，”Rotation”为72，也就是360/5=72，就可以实现按照星星的五个角重复绘制 最后就是添加动画，分别是Opacity透明度从0到1，Position从内到外，Transform变化大小 剩余的圆点，和短线的创建方法一致，就不再重复讲解～\n至此，显示的动画就创建好了。\n隐藏星星动画 再来看看隐藏星星的动画，有了上一小节的基础，隐藏星星的动画就容易多了，基本步骤都是一样的，如下：\n添加一个新的动画组hide 分别设置每个layer的动画，实心的星星从大到小、同时透明度降低，空心的相反，replicator layer一直隐藏 导出动画代码 动画都实现了好了以后，就可以导出代码了。\n依次File-\u0026gt;Export Code...，就可以打开导出代码的预览界面：\n右边的选项，按照需要选择，默认的基本可以满足要求，“Export Image Assets”最好选上，可以一同导出图片资源。选项的说明参见文档：http://www.quartzcodeapp.com/code-generation/code-options/\n至此，最复杂的动画部分就完成了，不用写一行代码，全部可视化编辑～\n调整、封装代码 接着就是封装导出的代码，方便使用。\nAPI设计 首先是整个动画的API的设计，为了方便给任意UIView实例添加动画，所以设计成UIView的Category，如下：\n// UIView+TTGStarAnimation.h @interface UIView (TTGStarAnimation) // 初始化动画 - (void)ttg_setupStarAnimation; // 初始化动画，同时可以调整inset - (void)ttg_setupStarAnimationWithEdgeInset:(UIEdgeInsets)inset; // 显示星星动画 - (void)ttg_showStarAnimated:(BOOL)animated complete:(void (^)())complete; // 隐藏星星动画 - (void)ttg_hideStarAnimated:(BOOL)animated complete:(void (^)())complete; // 当前是否显示 - (BOOL)ttg_getStarIsShow; @end AssociatedObject保存动画View实例 QuartzCode导出的动画代码都在一个View上，所以为了在Category里给任意UIView实例添加动画View的Property、当前星星是否显示的Bool值，就要用到AssociatedObject，对应的set、get方法如下：\n// UIView+TTGStarAnimation.m - (TTGStarAnimationView *)ttg_getStarAnimationView { return objc_getAssociatedObject(self, \u0026amp;kTTGButtonStarAnimationViewKey); } - (BOOL)ttg_getStarIsShow { return [objc_getAssociatedObject(self, \u0026amp;kTTGButtonStarAnimationStateKey) boolValue]; } - (void)ttg_setStarIsShow:(BOOL)isShow { objc_setAssociatedObject(self, \u0026amp;kTTGButtonStarAnimationStateKey, @(isShow), OBJC_ASSOCIATION_COPY_NONATOMIC); } 修改QuartzCode导出的代码 - 暴露内部Layer实例 QuartzCode导出的代码，内部的动画Layer是不可见的，为了能“手动”控制动画View的实际显示状态，就要修改下导出的代码，暴露内部的动画Layer实例。\n所有的Layer都在NSMutableDictionary * layers字典里面，通过key获取，给TTGStarAnimationView.h添加三个属性：\n// TTGStarAnimationView.h @interface TTGStarAnimationView : UIView @property (nonatomic, strong, readonly) CALayer *fillLayer; @property (nonatomic, strong, readonly) CALayer *emptyLayer; @property (nonatomic, strong, readonly) CALayer *replicatedLayer; // ... @end 然后实现getter方法：\n// TTGStarAnimationView.m - (CALayer *)fillLayer { return _layers[@\u0026#34;star_animation_fill\u0026#34;]; } - (CALayer *)emptyLayer { return _layers[@\u0026#34;star_animation_empty\u0026#34;]; } - (CALayer *)replicatedLayer { return _layers[@\u0026#34;replicator\u0026#34;]; } 初始化 ttg_setupStarAnimation和ttg_setupStarAnimationWithEdgeInset:方法，用来初始化动画，包括创建TTGStarAnimationView实例，添加、保存在当前view上，设置每个动画layer的状态等操作：\n// TTGStarAnimationView.m - (void)ttg_setupStarAnimation { [self ttg_setupStarAnimationWithEdgeInset:UIEdgeInsetsZero]; } - (void)ttg_setupStarAnimationWithEdgeInset:(UIEdgeInsets)inset { // 获取当前实例的动画starAnimationView TTGStarAnimationView *starAnimationView = [self ttg_getStarAnimationView]; if (!starAnimationView) { // 不存在，创建新的 starAnimationView = [[TTGStarAnimationView alloc] initWithFrame:UIEdgeInsetsInsetRect(self.bounds, inset)]; // 设置不可接受交互事件，防止阻断父view starAnimationView.userInteractionEnabled = NO; // 设置AutoSizingMask starAnimationView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; // 添加 [self addSubview:starAnimationView]; // AssociatedObject保存实例 objc_setAssociatedObject(self, \u0026amp;kTTGButtonStarAnimationViewKey, starAnimationView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } // 移除所有动画 [starAnimationView removeAllAnimations]; // 设置layer的初始化状态 starAnimationView.fillLayer.hidden = YES; starAnimationView.emptyLayer.hidden = NO; starAnimationView.replicatedLayer.hidden = YES; // 设置初始化状态值 [self ttg_setStarIsShow:NO]; } 动画控制 最后就是实现动画的控制，这里比较关键的是对动画的每个Layer的控制，每个Layer的初始、最终状态都要设置，保证多次调用、无论是否执行动画，最终显示都是一致的，以显示星星为例：\n// TTGStarAnimationView.m - (void)ttg_showStarAnimated:(BOOL)animated complete:(void (^)())complete { // 获取当前view的动画starAnimationView实例 TTGStarAnimationView *starAnimationView = [self ttg_getStarAnimationView]; // 当前是hide，才执行show if (![self ttg_getStarIsShow]) { // 设置状态值 [self ttg_setStarIsShow:YES]; // 先移除所有动画 [starAnimationView removeAllAnimations]; // 设置最终状态下Layer的hidden值的Block void (^completionBlock)() = ^ { starAnimationView.fillLayer.hidden = NO; starAnimationView.emptyLayer.hidden = YES; starAnimationView.replicatedLayer.hidden = YES; // 执行回调 if (complete) { complete(); } }; if (animated) { // 如果要执行动画，则设置动画前Layer的hidden值 starAnimationView.fillLayer.hidden = NO; starAnimationView.emptyLayer.hidden = NO; starAnimationView.replicatedLayer.hidden = NO; // 执行动画 [starAnimationView addShowAnimationCompletionBlock:^(BOOL finished) { completionBlock(); }]; } else { // 不用动画，直接执行最终状态 completionBlock(); } } } 隐藏的代码跟显示基本一致，把各种状态、hidden的值倒过来就可以了。 所有代码参见Github。\n要写的代码就这么多，是不是非常简单！\n总结 “工欲善其事,必先利其器”，借助各种强大的工具软件，做动画的效率简直提升几十倍～\n","date":"2017-03-08T00:00:00Z","image":"/assets/2017/03/08/quartzcode-demo-star-animation/quartzcode_demo_star_animation_1.gif","permalink":"/2017/03/08/quartzcode-demo-star-animation/","title":"用QuartzCode快速实现一个收藏动画"},{"content":"前言 最近抽空写了个拼图验证控件，用户可以通过水平、垂直，或者直接拖动拼图块，完成拼图图案，来完成验证。拼图块的形状可以自定义，默认提供了经典的拼图形状、圆形、正方形，整个拼图的图案也是支持从图片生成的。\nCocoaPods: pod \u0026quot;TTGPuzzleVerify\u0026quot; Github: https://github.com/zekunyan/TTGPuzzleVerify\n接下来，说说TTGPuzzleVerify的整体设计思路和实现原理。\n分析 组成部分 先直观上分析一下控件的组成部分：\n可以直观看出，控件应该由三个部分组成：\n拼图块：就是从原图上**“抠”**出来的拼图形状的图片块 原始图片：拼图整体图案的图片 镂空背景：从原始图片上抠出拼图块以后，给用户提示用的，透明度更低的拼图块镂空背景 拆解步骤 分析完了控件的组成，现在就来梳理一下控件的整个创建、交互流程。\n输入：图片、拼图形状、拼图大小、镂空的位置、拼图块的起始位置 生成拼图路径：根据拼图类型，生成对应的贝塞尔曲线 从图片抠出拼图块、镂空：有了拼图路径，就可以抠出拼图块和镂空的部分 设置位置、样式：根据参数设置拼图块的初始位置、样式，如阴影、镂空的透明度等 用户拖动：响应用户的头动手势，移动拼图块，根据坐标判断是否完成拼图 回调：用户完成拼图后，要回调，让外部响应拼图完成事件 关键技术点 从上面的步骤可以整理出来几个关键的技术点，如下：\n拼图的路径生成 普通的形状，如正方形、圆形之类的还好，代码实现比较方便，但是复杂的拼图图案怎么办？\n从图片上“抠出”拼图块、镂空 有了拼图的形状路径，如何从图片上抠出拼图块和镂空？\n用户拖动拼图、完成拼图验证 抠出了拼图块，如何将用户的手势对应到移动拼图块，最终完成验证？\n下面针对这三个点着重写下实现原理。\n实现 拼图的路径生成 既然手写代码太难，那就从现有的图案生成代码。\n1. 找SVG素材 既然要能随意改变大小，就要找矢量的图，Google “free svg”、“free icon”之类的，很容易就能找到免费的矢量图素材，例如TTGPuzzleVerify里面用的拼图形状就是从iconmonstr.com找来的。\n2. 编辑 直接下载回来的SVG还需要调整一下，如去除背景，我是用Sketch来编辑的。\n3. 生成代码 拼图的路径本质上就是拼图的轮廓，在Sketch里面把拼图SVG的背景色等颜色删除，然后就是将其转换为代码。 这里可以用PaintCode plugin for Sketch，将Sketch的拼图矢量路径导出成代码。\n4. 调整代码 直接生成的代码有时可能会不适用，可以人工调整下，然后就可以放到自己的类里面了：\n+ (UIBezierPath *)classicPuzzlePath { static UIBezierPath *puzzleShape = nil; static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, ^{ puzzleShape = [UIBezierPath bezierPath]; [puzzleShape moveToPoint:CGPointMake(17.45, 71.16)]; // 此处省略很多行=。=，PaintCode生成的 [puzzleShape closePath]; }); return puzzleShape; } 5. UIBezierPath拼图路径的大小、位置调整 直接导出的拼图路径的大小、位置是固定的，但是控件是要可以支持改变大小、位置的，所以，要对路径做变换，刚好UIBezierPath支持CGAffineTransform变换。\n大小变换：\n// 大小变换，path是生成的原始路径，_puzzleSize是传入的拼图块大小 [path applyTransform:CGAffineTransformMakeScale(_puzzleSize.width / path.bounds.size.width, _puzzleSize.height / path.bounds.size.height)]; 位置变换：\n// 位置变换，path是生成的原始路径，_puzzleBlankPosition是传入的拼图块、镂空位置 [path applyTransform:CGAffineTransformMakeTranslation( _puzzleBlankPosition.x - path.bounds.origin.x, _puzzleBlankPosition.y - path.bounds.origin.y)]; 至此，拼图的路径就写好了。\n从图片上“抠出”拼图块、镂空 1. 拼图块 有了UIBezierPath路径，抠出拼图块还是比较容易的，直接用CAShapeLayer来**“mask”**图片即可：\n// 拼图路径 UIBezierPath *puzzlePath = [self getNewScaledPuzzledPath]; // 创建mask layer CAShapeLayer *puzzleMaskLayer = [CAShapeLayer new]; puzzleMaskLayer.frame = self.bounds; puzzleMaskLayer.path = puzzlePath.CGPath; // Mask到图片imageView上 _puzzleImageView.layer.mask = puzzleMaskLayer; 效果就是：\n2. 镂空图片 抠出拼图块还是很容易的，但是“镂空”怎么办？ 其实道理是一样的，都是mask，只不过mask的区域反过来而已。 CAShapeLayer的fillRule就可以调整“填充的规则”，此处用kCAFillRuleEvenOdd就可以了，文档的解释如下：\nSpecifies the even-odd winding rule. Count the total number of path crossings. If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.\n理解过来就是：区域内一点，往外划一条线，经过奇数个数的交点，则点在区域内，偶数个数的交点，则在区域外。\n所以，只要在拼图路径的基础上，补充最外部的边框的路径即可：\n// 创建矩形边框路径 UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:self.bounds]; // 添加拼图路径 [maskPath appendPath:[UIBezierPath bezierPathWithCGPath:puzzlePath.CGPath]]; // 设置规则 maskPath.usesEvenOddFillRule = YES; // 创建mask的layer CAShapeLayer *frontMaskLayer = [CAShapeLayer new]; frontMaskLayer.frame = self.bounds; frontMaskLayer.path = maskPath.CGPath; frontMaskLayer.fillRule = kCAFillRuleEvenOdd; // 填充规则 // 设置mask _frontImageView.layer.mask = frontMaskLayer; 效果：\n3. 镂空背景 有了拼图块、镂空，还差一块，就是镂空的“半透明背景”，其实这个“半透明背景”和拼图块是一模一样的，所以不再重复代码，唯一增加的就是设置其透明度。\n4. 组合 将上面的组合到一起，就能形成最终的效果，从Reveal里面看到分层结构如下：\n其实就是三张一样的图片，按照不同的mask规则做了处理，叠加起来。\n用户拖动拼图、完成拼图验证 1. 拖动图片 有了上面的结构，拖动拼图的处理也就容易多了，只要移动最上层的拼图块就行了，这里直接用UIPanGestureRecognizer实现，移动的时候基于拼图的中心移动。\n// 创建手势 UIPanGestureRecognizer *panGestureRecognizer = [UIPanGestureRecognizer new]; [panGestureRecognizer addTarget:self action:@selector(onPanGesture:)]; [self addGestureRecognizer:panGestureRecognizer]; // 手势处理 - (void)onPanGesture:(UIPanGestureRecognizer *)panGestureRecognizer { // 获取坐标 CGPoint panLocation = [panGestureRecognizer locationInView:self]; // 拼图中心偏移 CGPoint position = CGPointZero; position.x = panLocation.x - _puzzleSize.width / 2; position.y = panLocation.y - _puzzleSize.height / 2; // 设置坐标 [self setPuzzlePosition:position]; // 判断、回调。。。 } 2. 验证用户的拼图是否完成 用户移动拼图时，只要判断拼图的坐标和镂空的位置是否一致就行。但是如果完全精确的判断，用户很难准确的把拼图拼对上，所以还要增加一定的**“tolerance”**，让拼图更加容易，同时暴漏出让外部设置：\n@property (nonatomic, assign) CGFloat verificationTolerance; // Verification tolerance, default is 8 判断的逻辑如下：\n// puzzlePosition是当前拼图块的位置，_puzzleBlankPosition是拼图镂空的位置 - (BOOL)isVerified { return fabsf([self puzzlePosition].x - _puzzleBlankPosition.x) \u0026lt;= _verificationTolerance \u0026amp;\u0026amp; fabsf([self puzzlePosition].y - _puzzleBlankPosition.y) \u0026lt;= _verificationTolerance; } 3. 完成拼图 当用户完成拼图后，外部会得到delegate、block的回调响应，接着就可以“完成拼图验证”，对此向外提供一个方法：- (void)completeVerificationWithAnimation:(BOOL)withAnimation，调用后拼图块将会移动回镂空位置，模拟出拼图完成的效果。\n至此，TTGPuzzleVerify的关键实现技术点就是这些了～\n最后 嗯，圣诞节快乐！ – 2016-12-25\n","date":"2016-12-25T00:00:00Z","image":"/assets/2016/12/25/TTGPuzzleVerify/blog_TTGPuzzleVerify_gif.gif","permalink":"/2016/12/25/TTGPuzzleVerify/","title":"开源项目-拼图验证控件TTGPuzzleVerify的实现"},{"content":"更新 2016-09-05: 新版0.3增加子Menu，整理选项；增加“History Wallpapers”打开历史图片文件夹；增加“Launch at startup”开机启动。 2016-09-03: 新版0.2.0！增加“Auto Update”选项；增加“Random”随机选择设置壁纸；修复自动更新不准的Bug。 极简的壁纸Mac App，每天自动下载最新微软Bing的每日壁纸并设置为桌面背景。\nGithub: https://github.com/zekunyan/TTGBingWallPaper\n由来 其实最开始只是很喜欢Bing的首页的壁纸图片，然后手动下载回来做壁纸，后来发现手动太麻烦，作为一个程序员，自己动手丰衣足食！=。= 然后花了个周末写了这个超简单的Mac App。\n安装 到Github的项目页面，即https://github.com/zekunyan/TTGBingWallPaper，从release里面下载最新的BingWallPaper.app.zip压缩包，解压出BingWallPaper.app，拖到Applications应用程序文件夹里面，打开即可。\n开发 用Swift 2.2写的，目前唯一依赖的库是SwiftyJSON/SwiftyJSON，需要Carthage。\ngit clone git@github.com:zekunyan/TTGBingWallPaper.git cd TTGBingWallPaper carthage update open BingWallPaper.xcodeproj 编译、安装、运行 未来 还有很多点可以做，一点一点来吧=。=\nTODO\n支持多国地区的Bing每日图片壁纸的获取 支持多语言 支持浏览历史图片 导出图片 … ","date":"2016-08-28T00:00:00Z","image":"/assets/2016/08/28/TTGBingWallPaper/BingWallPaper_banner.png","permalink":"/2016/08/28/TTGBingWallPaper/","title":"Swift开源Mac App - BingWallPaper"},{"content":"前言 第四篇来了。 还是3个小例子，仍然是主要部分用Masonry手写代码实现，其它的约束在storyboard里面直接拖拽搭建。 三个例子分别是：\n用约束优先级保证可移动View的内容可见 Autolayout的约束=控件间位置关系的“绑定” 利用layoutIfNeeded控制约束的生效时机 前三篇：\n第一篇：有趣的Autolayout示例-Masonry实现 第二篇：有趣的Autolayout示例2-Masonry实现 第三篇：有趣的Autolayout示例3-Masonry实现 Github地址： https://github.com/zekunyan/AutolayoutExampleWithMasonry\nCase 1: 用约束优先级保证可移动View的内容可见性 实现功能 Case 1实现的功能就是，可以用手指拖动一个灰色的Tip控件，在红框containerView内移动，并且保持移动到红框边缘时，Tip控件的内容不会超出红框，保证内容的可见性。\nFrame VS Autolayout 如果不用Autolayout的话，直接计算、设定frame，那么可能的步骤如下：\n获取手指当前触摸点在红框containerView内的坐标 计算Tip控件的宽高 根据宽高，计算、判断Tip控件会不会在新的位置上超出边界 调整新的Tip位置坐标 设置Tip控件的frame，指定新的坐标 这样去实现的话，代码将会比较“丑”=。。=，有大量的判断、数值计算，而且容易出错。\n而用Autolayout的话，只需要两步：\n设置约束 根据手指坐标，更新left、top两个约束，即更新Tip的位置 至于保证Tip内容在可见范围内，就交给Autolayout处理吧~\n关键原理 用Autolayout实现的关键，就是善于利用约束的优先级。先看看设置的约束示意图：\n中间的tipLabel就是被移动的控件\n褐色的两个约束，分别是Tip控件的centerX和centerY，与上级View的left和top的约束，用来对Tip定位 四个蓝色的约束，则是确保Tip内容不超出边界的约束 重点其实就在于，蓝色的约束的优先级比褐色的优先级高，这样，在移动中，系统会优先满足Tip的内容保持在边界内。\n实现细节 具体的设置约束代码如下：\n左边和顶部的约束 - 控制位置 首先设置两个属性，保存左边和顶部的约束，用于在移动时调整位置：\n@property (nonatomic, strong) MASConstraint *leftConstraint; @property (nonatomic, strong) MASConstraint *topConstraint; 然后就是设置具体的left、top约束，并设置优先级为750，也就是High\n[_tipLabel mas_makeConstraints:^(MASConstraintMaker *make) { // 优先级要比边界条件低 _leftConstraint = make.centerX.equalTo(_containerView.mas_left).with.offset(50).priorityHigh(); // 优先级要比边界条件低 _topConstraint = make.centerY.equalTo(_containerView.mas_top).with.offset(50).priorityHigh(); // ... }]; 四周的约束 - 保证内容在边界内 然后就是上下左右四个边界的约束：\n[_tipLabel mas_makeConstraints:^(MASConstraintMaker *make) { // 边界条件约束，保证内容可见，优先级1000 make.left.greaterThanOrEqualTo(_containerView.mas_left); make.right.lessThanOrEqualTo(_containerView.mas_right); make.top.greaterThanOrEqualTo(_containerView.mas_top); make.bottom.lessThanOrEqualTo(_containerView.mas_bottom); // ... }]; 这个地方要注意是lessThanOrEqualTo还是greaterThanOrEqualTo。\n滑动时更新Tip位置 这一点比较简单，直接将当前触摸的坐标赋值给leftConstraint和rightConstraint就好：\n_leftConstraint.offset = touchPoint.x; _topConstraint.offset = touchPoint.y; 小节 对比起来，明显是用Autolayout的方法比计算设置frame的方法要“优雅”的多=。=，灵活运用约束的优先级，往往可以减少很多工作量。\nCase 2: Autolayout的约束=控件间位置关系的“动态绑定” 实现功能 Case 2的功能其实是在Case 1的基础上实现的，就是在移动一个View的时候，另一个Attachment附件View，始终保持跟这个View的相对位置不变，就像是“附着”、“捆绑”在这个View上一样。\n关键概念：约束 = “动态的绑定” 之所以写这个例子，主要是想从概念上更加深入的理解Autolayout与传统的设置Frame的不同，即Autolayout里面的约束，其实是“动态的绑定”，是控件相互之间的位置关系绑定，约束是可以实时保证这种绑定关系的。\n传统的设置Frame，在View的内容、位置要变化时，是要手动设置的，是“一次性”的，而Autolayout，在确定了约束后，会实时的用约束的规则去更新Frame，所以可以避免这种手动设置。\n所以，对于本例子来说，设置Frame的方式和Autolayout的方式对比如下：\n用Autolayout的话，只要把Attachment的位置设置为相对于TipLabel就可以了。\n实现细节 代码比较简单，如下：\n[attachmentView mas_makeConstraints:^(MASConstraintMaker *make) { // 依附在tipLabel上 make.left.equalTo(_tipLabel.mas_left).with.offset(20).priorityHigh(); make.bottom.equalTo(_tipLabel.mas_top).with.offset(-2).priorityHigh(); }]; 移动Tip的相关代码看Case 1的就行~\n小节 本小节的Case比较简单，主要是为了着重点出“Autolayout约束=动态位置绑定”这个点，理解了这一点，才能更好地运用Autolayout~\nCase 3: 利用layoutIfNeeded控制约束的生效时机 实现功能 Case 3的例子，是一个灰色的View，初始时在屏幕中央，然后按下“执行动画”后，从左边滑入。\n步骤分析 直观上看，感觉是在按下按钮时，修改View的水平方向上的某个约束，然后用UIView的动画执行变化就好了。\n但是仔细分析，应该是先设置View的位置到屏幕的左边看不见的区域，然后用动画平滑的再让View移动回原位。\n就是说，只是从左边移动回来时，才让动画生效！问题就变成了，如何让约束分段执行，或者说，控制约束的生效时机。\n步骤可以由下图表示：\n实现关键 步骤分析清楚了，那么问题就是如何让约束在修改后，“立即”生效，而不是等到系统自动在下次更新布局时被动的生效。\n答案就是：利用**layoutIfNeeded**\n在更改了约束后，调用layoutIfNeeded，就可以通知系统立即刷新布局，从而更新Frame。如果要更加“保险”，可以在调用layoutIfNeeded前，加上调用setNeedsUpdateConstraints、setNeedsLayout，系统刷新布局时，就会先更新约束，然后根据约束重新计算相关View的Frame，想了解具体的过程的，可以参考：Mysteries of Auto Layout, Part 1 - WWDC 2015、Mysteries of Auto Layout, Part 2\n实现细节 具体的实现代码如下：\n// 设置初始状态，移动View到屏幕左边不可见区域 _centerXConstraint.equalTo(@(-CGRectGetWidth(self.view.frame))); // 立即让约束生效,刷新Frame [self.view layoutIfNeeded]; // 设置要动画的约束，移回原位 _centerXConstraint.equalTo(@0); // 执行动画 [UIView animateWithDuration:0.3f animations:^{ [self.view layoutIfNeeded]; }]; 小节 之前在实现TTGSnackbar的时候，就大量的使用了layoutIfNeeded来立即刷新View的Frame，然后才让产生动画的约束变动生效，也就是分阶段的让约束生效，非常管用~\n总结 不知不觉已经第四篇了，一共12个Case，都是平时开发时总结出来的点，以及对Autolayout的理解，希望可以一直将这个系列坚持下去~~\n","date":"2016-08-06T00:00:00Z","image":"/assets/2016/08/06/autolayout-example-with-masonry4/blog_autolayout_example_with_masonry4_1.gif","permalink":"/2016/08/06/autolayout-example-with-masonry4/","title":"有趣的Autolayout示例4-Masonry实现"},{"content":"译者前言 总是看到有人说用汇编实现objc_msgSend是为了速度快，当然这个不可否认。但是难道没有别的原因？于是就看到了这篇文章，遂翻译之！=。=\n我自己的理解就是，用汇编实现，是为了应对不同的“Calling convention”，把函数调用前的栈和寄存器的参数、状态设置，交给编译器去处理。 先看看原文吧。\n原作者: Ari Grant 原文链接: Why objc_msgSend Must be Written in Assembly\n开始 对于Objective-C来说，调用一个对象实例的方法，也叫作向这个对象实例**“发送消息”**，而每条“消息”，在编译阶段都会转变为一次对objc_msgSend函数的调用，调用的参数不仅有原本消息的所有参数，还有消息的接收者receiver和对应的方法selector。举个例子，下面的语句：\n[receiver message:foo beforeDate:bar]; 将会被编译成：\nobjc_msgSend(receiver, @selector(message:beforeDate:), foo, bar); 对于objc_msgSend函数的实现原理，前人已经做了大量的探索。所以，本文将会把重点放在objc_msgSend的一个之前没有太受到关注的点上，那就是：\nobjc_msgSend是不可能用Objective-C、C或者C++实现的。\nTHE RETURN TYPE - 返回类型 先看看如下两行代码：\nNSUInteger n = [array count]; id obj = [array objectAtIndex:6]; 直观上看，将会被编译成：\nNSUInteger n = objc_msgSend(array, @selector(count)); id obj = objc_msgSend(array, @selector(objectAtIndex:), 6); 但是实际上这是不可能的，因为没有函数可以同时满足这两个调用。而且它的返回值也不能同时是NSUInteger和id。\n而且，上面的代码也是无法编译通过的。那么，加上类型转换怎么样？\nNSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array, @selector(count)); id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6); 这下可以编译通过了，虽然看起来不直观。。。\nobjc_msgSend是一个Public的函数，在\u0026lt;objc/message.h\u0026gt;里声明，如果你想直接调用它，就必须按照上面的格式加上强制类型转换，要不然是无法编译通过的。但是objc_msgSend到底是如何实现，来支持各种返回类型的？本文后面会讲到。\nTHE IMP - 方法对应的函数指针 objc_msgSend函数的本质很简单，传入一个接受者对象实例receiver和方法名selector，它就会按照以下步骤执行：（译者注：只是最粗略的步骤=。=）\n获取receiver得类Class 在Class的方法列表method table里面查找对应selector的方法实现 找到的话就调用，返回 找不到就在其父类中找，重复前面的步骤（直到没有父类为止） 整个流程很简单，沿着继承链，向上找到方法selector对应的函数指针即可，也就是IMP。同时，在每层Class中都有缓存，加快后续的方法查找。但是，这也只是objc_msgSend的实现细节，所以，接着往下看。\nTHE ARG TYPES AND COUNT - 参数类型和数量 简单来说，当objc_msgSend找到对应的函数指针后，只要用传入的参数调用这个函数即可。剩下来的就是找到一种方法，可以调用任意参数类型、数量的任意函数。\n参数的数量很容易计算。然后我们可以把所有的参数都放入varargs，然后调用函数时传入即可。但是这样的话，每个Objective-C的方法都必须在其prologue（译者注：函数执行具体的“任务”前，所做的准备环节）里面把所有的参数从varargs里面提取出来。\n这种把参数打包到varargs里面然后又取出来的办法显然是非常糟糕的，同时也是不必要的。\n在C语言中，调用一个函数会被编译成对应的汇编语言指令，首先是设置参数（把参数放到寄存器、栈上），然后用如jump或者call的指令，跳到具体的函数代码地址处。如果我们想支持任意类型的函数类型，我们就必须写一个switch语句，把所有的参数组合情况都包含起来，这样才能正确的为任何形式的函数设置参数（译者注：即按照某种**“规范”、“约定”**，把参数依次存放到“约定”的寄存器、栈上），这显然是没有扩展性的，更是不可能的。\nUNWINDING THE CALL - 拆解调用 objc_msgSend的解决办法，主要依据的是：当objc_msgSend被调用时，所有的参数已经被设置好了。\n换一种方式来说，就是：在objc_msgSend开始执行时，栈帧（stack frame）的状态、数据，和各个寄存器的组合形式、数据，跟调用具体的函数指针（IMP）时所需的状态、数据，是完全一致的！\n如下这行代码：\nid obj = objc_msgSend(array, @selector(objectAtIndex:), 6); 在调用objc_msgSend时，需要设置三个参数，分别是被调用方receiver、方法名selector和最后一个整型参数6。这和具体的方法函数IMP的参数顺序、类型是完全一致的，也就是说，调用objc_msgSend前，设置的栈、寄存器的状态、数据正是调用具体的方法函数时需要的状态！\n所以，当objc_msgSend找到要调用的函数实现IMP后，只需要把所有的对栈、寄存器的操作“倒”回到objc_msgSend执行开始的状态（类似于函数执行完成return返回前，做的“收尾处理”工作一样，即epilogue），直接jump/call到IMP函数指针对应的地址，执行指令即可，因为所有的参数已经被设置好了。\n同时，当selector对应的IMP执行完成后，返回值也被正确的设置好了（在x86平台上，返回值被设置到了指定的寄存器eax/rax里，在arm上，则是r0寄存器），所以，我们也不必担心前文提到的不同类型的返回值问题了。\nWRAP UP - 总结 把上面提到的所有解释综合起来，就是：在C语言里面调用函数，必须在编译时就知道调用的“状态”；而这些“状态”在运行时是无法得出或正确处理的，所以必须往底层走，用汇编处理。（译者注：这里不知道咋翻译好=。=，原文是：calling a function in C requires the signature to be known for each call-site at compile-time；doing so at run-time is not possible and so one must drop down into assembly and party there instead.）\nUPDATE - 后续 有人指出objc_msgSend有可能是用GCC的扩展方法__builtin_apply_args，__builtin_apply，和__builtin_return实现的。这也正指出了一个事实，就是这些builtins方法是非常有必要的，因为单靠语言本身无法实现这些功能。实现objc_msgSend所需要的技巧，也正是实现这些builtins方法所需要的技巧。本文的目的并不是非要将什么是真正的C、什么不是真正的C分个清楚，只是为了指出objc_msgSend特殊罢了。\n译者总结 开头也说了，我的理解是：用汇编实现，是为了应对不同的“Calling convention”，把函数调用前的栈和寄存器的参数、状态设置，交给编译器去处理。\n嗯，以后不要再说用汇编实现只是为了快了=。=\n","date":"2016-06-19T00:00:00Z","permalink":"/2016/06/19/translation-why-objcmsgsend-must-be-written-in-assembly/","title":"翻译-为什么objc_msgSend必须用汇编实现"},{"content":"前言 RESTful API的设计已经很成熟了，大家也都比较认可。本文也不再过多介绍RESTful API相关的知识，而是针对JSON型API的返回结果设计，总结下自己的经验。\n结构 先来看看返回结果的结构示例：\n{ data : { // 请求数据，对象或数组均可 user_id: 123, user_name: \u0026#34;tutuge\u0026#34;, user_avatar_url: \u0026#34;http://tutuge.me/avatar.jpg\u0026#34; ... }, msg : \u0026#34;done\u0026#34;, // 请求状态描述，调试用 code: 1001, // 业务自定义状态码 extra : { // 全局附加数据，字段、内容不定 type: 1, desc: \u0026#34;签到成功！\u0026#34; } } data字段 - 请求数据 首先是本次请求结果的数据data字段，其值为对象（字典）或数组均可以，根据业务而定。\n如请求的是某个用户的个人profile信息，就可以是对象，对象里面是用户profile的键值对数据，如user_id: 123、user_name: \u0026quot;tutuge\u0026quot;等。\n如果请求的是列表数据，就可以是数组，如请求用户列表：\ndata: [ {user_id: 123, user_name: \u0026#34;tutuge\u0026#34;}, {user_id: 321, user_name: \u0026#34;zekunyan\u0026#34;}, ... ] 数组、对象，相互嵌套，灵活组合即可。\n对于iOS来说，解析data字段是对象还是数组也很容易，在接收到JSON数据字典后，如AFNetworking的返回结果，对data判断其类型即可：\nif ([jsonDict[@\u0026#34;data\u0026#34;] isKindOfClass:[NSDictionary class]]) { // JSON对象 } else if ([jsonDict[@\u0026#34;data\u0026#34;] isKindOfClass:[NSArray class]]) { // JSON数组 } msg字段 - 请求状态描述，调试用 msg字段是本次请求的业务、状态描述信息，主要用于调试、测试等。\n如“done”、“请求缺少参数！”\n服务端可以自由发挥，开发人员看得懂就好。 -_-|||\ncode字段 - 业务自定义状态码 code字段，业务自定义的状态码。\n其实是否要在API里面自定义业务状态码，非常有争议=。=，因为Http请求本身已经有了完备的状态码，再定义一套状态码直观上感受却是不对劲。但是实际开发中，确实发现自定义业务状态码的必要性，如一次成功的Http status 200的请求，可能由于用户未登录、登录过期而有不同的返回结果和处理方式，所以还是保留了code。\n状态码的定义也最好有一套规范，如按照用户相关、授权相关、各种业务，做简单的分类：\n// Code 业务自定义状态码定义示例 // 授权相关 1001: 无权限访问 1002: access_token过期 1003: unique_token无效 ... // 用户相关 2001: 未登录 2002: 用户信息错误 2003: 用户不存在 // 业务1 3001: 业务1XXX 3002: 业务1XXX // ... Code业务状态码最好是用常量定义的，当然有能力的动态配置更新更好，这里就不再详细说明。\nHttp的状态码参考：List of HTTP status codes\nextra字段 - 全局附加数据 extra字段，用来表示全局的附加数据。\n这个字段来源于之前做项目时，用户的操作（数据请求），会导致用户的等级、经验变化，而具体什么时候产生不确定，由服务端的规则决定，并且客户端要及时向用户展示变化，所以加上了extra字段。\n在设计extra字段的时候，并没有对其结构内容做限制，所以比较灵活，但是还是要有个type字段，来做约束，如：\n// 升级 type: 1, show_msg: \u0026#34;恭喜您升级到XXX\u0026#34; // 完成任务 type: 2, task_desc: \u0026#34;达成XXX成就\u0026#34; 总的来说就是自由发挥，只要服务端、客户端相互沟通好即可。当然，也要避免乱用，保证真的需要全局附加数据才使用这个字段。\n最佳实践 总结几点最佳实践。\n规范统一的命名 命名风格统一\n不管是驼峰式还是下划线式，统一就好。当然，按照目前的“大众规范”，还是统一小写加下划线比较好=。= 如：user_id，user_name，user_age等。\n语义清晰，遵守常用缩写\n字段的名字最好能体现字段的类型，遵守一些“常用”的缩写，如：\n// 字符串 user_name, task_desc, date_str, article_title, feed_content 等 // 数字 user_id, users_count, task_num, xxx_offset 等 // 日期 login_at, create_date, logout_time 等 // 布尔 is_done, is_vip, protected, can_read 等 // URL user_avatar_url, thumb_url 等 // 数组 users, profiles, thumb_imgs 等 空值、空字段的处理 空值、空字段的处理也是比较容易出问题。\n统一空值用null\n除了布尔类型的，其余的空值统一用null表示，客户端保证每种字段的null可以被正常处理。\n给不同类型设置默认空值\n除了null，还可以对字段设置“默认值”，如数字就是0，字符串就是空字符串\u0026quot;\u0026quot;，数组就是空数组[]，对象就是空对象{}，这样有个好处就是可以避免很多客户端（Java、OC）处理空值（Null、nil、null）产生的异常。但是危害就是容易语义不明。还是要根据具体业务、前后端约定而定。\n以前写过一篇用Runtime的手段填充任意NSObject对象的nil属性，其实就是为对象空值统一设置默认值的=。=，可以参考。\n布尔boolean值的处理 说实话，我见过各种布尔值表示方式，如：\nis_login: true, is_login: \u0026#34;true\u0026#34;, is_login: 1 is_login: \u0026#34;TRUE\u0026#34; is_login: \u0026#34;YES\u0026#34; // ...天啊 由于语言本身的限制、框架的处理方式，不对布尔类型的值做限制总觉得不踏实，像C、C++、Objective-C里面的布尔就是数字0和1，其它语言也都各自不一样，还有从数据库读写导致的布尔值类型不一致等。\n所以，如果可以的话，最好一开始就对所有请求参数、结果的布尔值类型做限定，个人觉得统一成数字0和1最好。\n然后在客户端和服务端统一设置常量、宏定义，定义布尔的类型，所有的参数、结果的布尔字段全部做强制约束。\n时间、日期字段 时间的处理也是非常容易出错的，特别是遇上时区转换的时候。\n强制GMT/UTC时间戳\n一种做法就是强制所有时间参数只能传Unix时间戳，也就是标准GMT/UTC时间戳，然后由各自的客户端根据自己的时区、显示要求做处理后显示。\n// 从服务器接收的时间数据 login_at: 1462068610 // 根据时区、显示要求转换，如北京时间 显示：2016年5月1日下午1点、1天前等 这样的话，客户端、服务端存储、读取时间都相当于处理纯数字。\n使用ISO 8601带时区的时间日期字符串\n使用Unix时间戳有个坏处，就是：\n最早只能到1970/1/1 0:0:0GMT时间，一旦需求早于这个时间，时间戳就成了负数=。= 不方便人阅读。调试API的时候，开发人员不能直观看出具体时间，很不方便 所以，可以按照ISO 8601标准，用字符串保存、传输时间。\n如果以YYYY-MM-DDThh:mm:ssTZD格式为准, 时间的形式就是1997-07-16T19:20:30+01:00，保存了时区信息，也方便阅读。\ntype类型的处理 API数据中免不了各种类型字段，如用户类型user_type、登录类型login_type等，类型的表示也可以分为数字、字符串两种。\n数字表示类型\n这个应该是最直接的方式了，客户端和服务端共同维护某个API下、某个数据类型中的type常量，靠文档约束。\n字符串表示类型\n数字的类型毕竟不利于直观阅读，如果可以的话，用字符串也是不错的，当然坏处就是代码里面就不能用Switch语句了（除了强大的Swift=。=）\n// 如登录类型，QQ、微信、微博等 login_type: \u0026#34;qq\u0026#34;, login_type: \u0026#34;wechat\u0026#34;, login_type: \u0026#34;sina_weibo\u0026#34;, 完整的URL API里面的数据也会有URL类型的，一般来说如用户的头像、各种图片、音频等资源，都是以URL链接的形式返回的。\n返回的URL一定要“完整”，主要指的是不要忘记URL里面的协议部分，也就是scheme部分。\n像tutuge.me/imgs/1.jpg这种URL值，就是不完整的，没有指明网络协议，难道靠猜=。= 应该是http://tutuge.me/imgs/1.jpg。\n总结 嗯，规范非常重要。:-D\n","date":"2016-05-02T00:00:00Z","permalink":"/2016/05/02/design-json-api-respoense/","title":"API返回结果设计经验与总结"},{"content":"更新 2016-05-01: 补充了一点本文涉及的汇编知识\n前言 本文结合一段访问Out Parameters出现了EXC_BAD_ACCESS错误的代码，通过反编译等手段验证Objective-C中__autoreleasing的一些特点。\n2016-05-01更新 - 关于本文的反编译汇编代码 本文的反编译基于MachO 64bits，即System V X86_64，读懂本文需要的最简单Calling convention如下：\n函数参数顺序：\nrdi, rsi, rdx, rcx等寄存器 对应到 id objc_msgSend ( id object, SEL cmd, arg1, arg2 ... ); 就是 rdi = object # 被调用对象 rsi = cmd # 方法selector rdx = arg1 # 方法第一个参数 rcx = arg2 # 方法第二个参数 函数返回值：\n本文只涉及: rax寄存器 详细可参考：\niOS ABI Function Call Guide OS X ABI Function Call Guide OS DEV Calling Conventions Out Parameters - 指针的指针 所谓Out Parameters，其实就是指针的指针，熟悉C/C++的朋友应该不陌生，通过指针的指针可以改变指针的值，在Objective-C里面很多地方用到了这种方法，来在函数、方法内部改变参数的原始值，如：\n// NSFileManager的方法 - (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error 第二个参数(NSError **)error，就是Out Parameters，调用时传递NSError类型指针的地址即可：\nNSError *error = nil; [[NSFileManager defaultManager] removeItemAtURL:url error:\u0026amp;error]; // 处理error... 出现EXC_BAD_ACCESS的代码示例 假设有如下遍历数组检查数值零的方法：\n- (void)checkZeroInArray:(NSArray \u0026lt;NSNumber *\u0026gt; *)array error:(NSError **)error { // 遍历 [array enumerateObjectsUsingBlock:^(NSNumber * _Nonnull number, NSUInteger index, BOOL * _Nonnull stop) { if (number.integerValue == 0) { // 检查 if (error) { // 指针是否有效 *error = [NSError errorWithDomain:@\u0026#34;me.tutuge\u0026#34; code:100 userInfo:nil]; // 创建NSError实例 } *stop = TRUE; // 停止遍历 } }]; } 调用的时候如下：\nNSError *error = nil; [self checkZeroInArray:numbers error:\u0026amp;error]; // 取地址 if (error) { NSLog(@\u0026#34;Error: %@\u0026#34;, error); // EXC_BAD_ACCESS错误 } 可以得出，NSLog(@\u0026quot;Error: %@\u0026quot;, error)访问error的时候，error的地址指向的内存空间已经被释放，所以才会出现EXC_BAD_ACCESS错误。\n但是为什么会被释放？什么时候被释放的？经过一番查证，发现跟__autoreleasing和@autoreleasepool有关。\n下面先对__autoreleasing做点研究=。=\n__autoreleasing - 变量所有权(ownership)修饰符 先看看__autoreleasing的定义及一些特点。\n__autoreleasing是变量所有权修饰符的一种，除了它，还有__strong、__weak和__unsafe_unretained，详细的说明可以参考：Clang文档-Objective-C Automatic Reference Counting (ARC)\n简单来说，就是被__autoreleasing修饰的变量会被加入到当前的autoreleasepool中，可以理解为如下两段分别在ARC和MRC中的代码等价：\n// ARC id __autoreleasing obj = someObj; // MRC id obj = [[someObj retain] autorelease]; 再进一步，除开各种影响因素，假设有如下函数：\n- (void)funcWithObj:(id)someObj { id __autoreleasing obj = someObj; } 用Hopper Disassembler反编译后为如下汇编代码(MachO 64bits)：\nvar_18就是obj变量：\nlea rax, qword [ss:rbp+var_18]和mov rdi, rax取了var_18的地址放在rdi寄存器中，mov rsi, rdx将someObj值放到了rsi寄存器中，然后调用id objc_storeStrong(id *object, id value)函数最终将someObj值保存在var_18变量中。\n__autoreleasing导致了id objc_retainAutorelease(id value)函数的调用：\ncall imp___stubs__objc_retainAutorelease，就是对obj变量，也就是var_18，调用了objc_retainAutorelease函数，先retain然后autorelease了一次，其实现大致如下：\nid objc_retainAutorelease(id value) { return objc_autorelease(objc_retain(value)); } objc_storeStrong和objc_retainAutorelease可参考：Clang文档-Objective-C Automatic Reference Counting (ARC)\n可验证，用__autoreleasing修饰的变量会被添加到当前的autoreleasepool中。\n方法的Out Parameters参数会自动添加__autoreleasing属性 当方法参数里面有Out Parameters参数时，就是有指针的指针类型时，编译器会自动为参数加上__autoreleasing属性，如以下两个方法：\n- (void)generateError1:(NSError **)error { *error = [NSError new]; } - (void)generateError2:(NSError * __autoreleasing *)error { *error = [NSError new]; } 编译时，generateError1:会对参数error自动添加__autoreleasing，然后就跟generateError2:的实现完全一致了。\n通过反汇编也可看出两者完全一致：\n在call imp___stubs__objc_msgSend完成后，rax寄存器保存了[NSError new]的对象，然后mov rdi, rax，转移到rdi寄存器，作为objc_autorelease函数的参数被调用，*error被加到了当前的autoreleasepool中。\n如果传给Out Parameters参数的变量没有用__autoreleasing修饰，编译器会创建一个临时变量并以__autoreleasing修饰再传入 根据苹果的Transitioning to ARC Release Notes文档可知，如果有如下调用:\nNSError *error; // 默认__strong类型 [self generateError1:\u0026amp;error]； 编译器检测到generateError1:方法的Out Parameters类型参数，但是调用时的error又不是__autoreleasing修饰的，就会自动创建一个__autoreleasing修饰的临时变量，用来代替error传入，编译器重写后如下：\nNSError * error; // 默认__strong类型 NSError * __autoreleasing tmp = error; [self generateError1:\u0026amp;tmp]； 通过汇编来验证一下：\n假如有如下调用：\n- (void)runTest { NSError *error = nil; [self generateError1:\u0026amp;error]; // 就是上面的实现 } 反汇编后，如下：\n代码有点多=。=，从图中汇编可知：var_20就是自动生成的临时变量，var_18是我们定义的error变量。\nmov qword [ss:rbp+var_18], 0x0对var_18做初始化，也就是赋nil值，然后mov rdi, qword [ss:rbp+var_18]和mov qword [ss:rbp+var_20], rdi就是用var_18初始化了var_20临时变量。\nlea rdx, qword [ss:rbp+var_20]将var_20临时变量的地址存到了rdx寄存器中，作为objc_msgSend实现generateError1:调用时的第三个参数，也就是(NSError **):error参数，完成调用。\n调用完成后，通过如下调用，将临时变量var_20的值保存到var_18，即error变量中。\nlea rdx, qword [ss:rbp+var_18] # var_18为objc_storeStrong第一个参数 mov rsi, qword [ss:rbp+var_20] # var_20为objc_storeStrong第二个参数 mov rdi, rdx call imp___stubs__objc_storeStrong 开头例子出现EXC_BAD_ACCESS错误的原因 经过上面一番对__autoreleasing的总结，再来看看开头例子的错误原因就比较容易懂了。\nenumerateObjectsUsingBlock会在循环内部自动添加autoreleasepool\n首先应该明确的就是enumerateObjectsUsingBlock:在用block迭代遍历NSArray的元素时，会自动添加autoreleasepool，对于例子来说，相当于：\n// 枚举遍历 [array enumerateObjectsUsingBlock:^(NSNumber * _Nonnull number, NSUInteger index, BOOL * _Nonnull stop) { if (number.integerValue == 0) { if (error) { *error = [NSError errorWithDomain:@\u0026#34;me.tutuge\u0026#34; code:100 userInfo:nil]; } *stop = TRUE; } }]; 结合__autoreleasing后，重写为：\nNSNumber *number = nil; BOOL stop = FALSE; for (NSUInteger index = 0; index \u0026lt; array.count \u0026amp;\u0026amp; !stop; index++) { @autoreleasepool { // 自动添加 number = array[index]; if (error) { *error = [[NSError errorWithDomain:@\u0026#34;me.tutuge\u0026#34; code:100 userInfo:nil] autorelease]; // 注意这个autorelease ! } stop = TRUE; } } 而autorelease对应的函数id objc_autorelease(id value)的官方解释：\nIf value is null, this call has no effect. Otherwise, it adds the object to the innermost autorelease pool exactly as if the object had been sent the autorelease message.\n其中的innermost autorelease pool表示的就是“最内层”的autoreleasepool，对于例子来说就是*error被添加到了循环内的autoreleasepool中，当然，导致的结果就是本次循环结束后，*error也随着一起被释放了。\n最终导致了外部访问了已经被释放的*error，出现了EXC_BAD_ACCESS错误。\n总结 很多时候不能想当然的写代码=。=，要不然出了问题找都找不到，每个细节都很重要。 嗯，现在读汇编快多了。。。\n参考 Clang document: Objective-C Automatic Reference Counting (ARC) Transitioning to ARC Release Notes 黑幕背后的Autorelease ","date":"2016-04-30T00:00:00Z","image":"/assets/2016/04/30/autoreleasing-meet-autoreleasepool/autoreleasing-meet-autoreleasepool_1.jpg","permalink":"/2016/04/30/autoreleasing-meet-autoreleasepool/","title":"结合访问Out Parameters出现EXC_BAD_ACCESS的例子，反编译汇编解读__autoreleasing"},{"content":"前言 总结一些组织代码的方法，大到各个子项目模块之间，小到单个文件内部，涉及到了CocoaPods私有库、Carthage构建私有Framework、项目内Group和文件夹、类内用Category组织、文件内的#pragam mark、// MARK -。虽然有些老生常谈，就当是重新梳理一遍吧=。=。\n多个子项目 - CocoaPods加私有仓库 感觉目前CocoaPods已经成了搞iOS开发的标配了，基本上所有的第三方库和组件都可以通过CocoaPods来集成管理。但是CocoaPods的功能其实不止管理第三方库和组件，团队内部的拆分出来的子项目和子模块，同样可以用CocoaPods来集成管理。\n先看看如下Podfile的示例：\n# 源配置 # source \u0026#39;git@gitlab.xxx.com:cocoapods/specs.git\u0026#39; # 团队内部Gitlab源 source \u0026#39;https://git.coding.net/xxx/specs.git\u0026#39; # Coding等私有Git托管平台 # iOS版本支持 # platform :ios, \u0026#39;7.0\u0026#39; # 开发中的子项目 # pod \u0026#39;XXXCommonUI\u0026#39;, :path=\u0026gt; \u0026#39;/Users/tutuge/projects/Xcode/XXXCommonUI\u0026#39; # 本地路径 # 依赖的内部子项目 # pod \u0026#39;XXXModuleA\u0026#39;, :git=\u0026gt; \u0026#39;git@gitlab.xxx.com:cocoapods/XXXModuleA\u0026#39;, :branch=\u0026gt;\u0026#39;develop\u0026#39; pod \u0026#39;XXXModuleB\u0026#39;, \u0026#39;1.0.1\u0026#39; # 依赖的开源第三方库 # pod \u0026#39;AFNetworking\u0026#39;,\u0026#39;3.0.0\u0026#39; pod \u0026#39;DTCoreText\u0026#39; # 额外的脚本 # # ... 私有Git仓库\n虽然我们有万能的Github了，但是还是有些代码是不能公开的=。=，所以需要一个私有的Git仓库。\n有自己服务器的，完全可以用Gitlab(或者Gogs)搭建一个功能强大的私有Git仓库，管理项目代码，搭建CocoaPods私有源都很不错。\n当然，懒得折腾的，也可以用第三方的私有Git服务，如：Bitbucket、Coding等。\n私有CocoaPods源\n搭建一个私有CocoaPods源其实就是创建一个私有的Spec Repo，说白了就是一个存放了所有私有Pods的Podspec文件的，按照特定结构组织的目录，然后把这个目录推到私有Git仓库就成了一个私有的CocoaPods源。\nSpec Repo的目录结构示例如下：\n├── Specs └── [XXXModuleA] └── [0.0.1] └── [XXXModuleA].podspec.json └── [0.0.2] └── [XXXModuleA].podspec.json 然后添加私有源即可：\npod repo add XXXSpecRepo https://git.coding.net/xxx/specs.git 创建私有CocoaPods项目的具体步骤\n至于具体的创建步骤，CocoaPods的官方文档和一些优秀的博客已经说得很清楚了，我就不再重复了。推荐阅读：CocoaPods Guides，还有使用Cocoapods创建私有podspec，这篇很详细！\n引用正在本地开发的子项目\n主工程依赖的子项目也是可以同时开发的，用path指定路径即可，如pod 'XXXCommonUI', :path=\u0026gt; '/Users/tutuge/projects/Xcode/XXXCommonUI'，然后pod install一下，依赖的本地工程就会集成到主工程的Development Pods目录下，开发时，就可以同时修改主项目和子项目了。\n构建私有Framework - Carthage 除了抽离出子项目，用Framework的方式来管理公共代码也是不错的选择，Framework的好处也是多多的:\n本质上是个bundle，不仅可以打包代码，还可以打包资源Assets、视图XIB等 可以有选择的开放头文件、接口，避免了用CocoaPods集成子项目时，所有代码全部暴露的问题，只暴露需要的，代码更加清晰。 从iOS 8开始支持的Cocoa Touch Framework可以支持动态Framework，能在主App和各种Extension之间共享一份二进制目标代码，加快应用加载速度，减小体积，加快编译速度等。 基于Cocoa Touch Framework动态框架的种种优点，“去中心化”的轻量级依赖包管理工具Carthage也越来越受欢迎。\n总的来说，Carthage没有CocoaPods那样的Spec Repo中心，不会对现有项目工程修改，只是完成了拉取代码、构建Framework的工作，非常的轻量级，所以用来构建团队内公用的Framework非常不错~\n创建Framework\n这里的Framework指的是Cocoa Touch Framework动态框架，静态Framework的缺点不少(如不能共享已加载到内存的二进制目标代码、构建麻烦等)，如果项目可以从iOS 8开始支持，为啥不用动态框架呢~\n创建Framework的过程非常简单，Xcode 6以后就有了Framework项目模板，直接选中”Cocoa Touch Framework”模板，按照提示来即可。记得在TARGETS-\u0026gt;Build Phases-\u0026gt;Headers-\u0026gt;Public\u0026quot;加入要暴露的头文件（Swift的话，将要暴露的类、方法标记public）就好。\n用Carthage构建Framework\nCarthage只支持构建标记为“Shared”的项目Target，所以创建好了Framework工程后，还要设置Scheme为“Shared”，可以在当前Scheme的Edit Scheme里面设置，也可以在Xcode的菜单Product-\u0026gt;Scheme-\u0026gt;Manage Schemes里面设置。\n然后就可以将Framework工程推送到Git仓库中，可以是Github，也可以是私有Git仓库，如上一节提到的Coding、Bitbucket等，只要本机可以正常访问即可。记住打上版本号tag。\n然后就可以在一个测试工程中创建一个Cartfile文件，输入Framework的Git路径，按照官方文档设置一下”Run Script”，运行carthage update生成对应的Framework，就可以集成测试。\n最终的Cartfile\n由于Carthage的去中心化，用Carthage管理项目的Framework依赖就非常灵活，一个简单的示例如下：\n# 开发中的本地Framework git \u0026#34;file:///directory/to/project\u0026#34; \u0026#34;develop\u0026#34; # 依赖的私有Framework git \u0026#34;git@gitlab.xxx.com:cocoapods/XXXFramework.git\u0026#34; \u0026#34;development\u0026#34; # 第三方开源库 github \u0026#34;Mantle/Mantle\u0026#34; ~\u0026gt; 1.0 所以，Carthage可以方便的管理构建本地、私有、第三方的Framework库，而且更加方便~\n详细的创建流程Google一下就有很多，如Build your own Cocoa Touch Frameworks, in pure Swift、Creating your first iOS Framework、How to Make a Carthage-compatible Framework in Swift。\n和CocoaPods混用\nCarthage是没有任何侵入性的，不会对项目工程有修改，只是构建出了Framework文件，所以完全可以把Carthage当做纯粹的Framework构建工具，跟CocoaPods一起愉快的使用=。=\n项目内文件组织 - Group和文件夹 项目内的代码组织，其实就是代码文件的组织，总的来说，其实有以下三种：\n具有物理文件夹结构的Folder reference 从Xcode创建的逻辑上的Group 具有物理文件夹结构的Group 排除Folder reference\nFolder reference说白了就是硬盘上一个文件夹的逻辑引用，有点像Linux文件系统里面的软连接=。=，图标是蓝色的，将一个文件夹拖到Xcode中时，Create folder references选项创建的就是。\n虽然编译时也会被打包到程序中，更能同步真正文件夹内部的修改，但是遗憾是的里面的代码文件是不能自动的加入到Xcode的编译中的，要手动一个一个加，而且图片资源也不能在IB里面使用，代码里面使用也要加上路径，所以一般是纯资源文件夹用Folder reference的方式引用，对于组织代码文件，就可以排除Folder reference的方式。\n纯逻辑Group文件平铺，脱离Xcode不方便查看\n首先要明确的是，Group是Xcode对文件的一种逻辑上的引用，并且在编译时Xcode会把所有的Group展开，所以代码里面也就不用像Folder reference一样加上路径，还可以针对Target作区分，很方便。\n但是Xcode中创建的Group并没有对应到文件系统中的文件夹，导致一旦脱离Xcode看代码，所有的文件都平铺在了根目录下，非常不方便。\n难道没有办法了？\n有物理文件夹结构的Group最好\n还有一种Group是可以有物理文件夹结构的，其实将一个文件夹拖到Xcode中，选择Create groups for any added folders创建的Group就是既有Group的有点，又可以保持物理文件夹结构。这样无意是最好的~\n手动创建: 可以Add Files To XXXX，然后选择New Folder创建文件夹，然后把代码文件拖进去，再添加回Xcode。\n自动创建： 对于现有的大量的Group，手动一个一个创建实在不方便，还好有个命令行工具：synx，直接在工程根目录运行synx XXX.xcodeproj就可以自动为Group创建物理文件夹。 当然，CocoaPods管理的库要重新pod install一次，手动添加的Framework也要重新添加。\n类内代码组织 - Category Objective-C中的Category实在是太方便了，可以灵活的为现有的类增加方法，甚至是系统的类。\n但是其实Category也可以拿来组织一个类的实现，将类的Property、方法按照业务、功能拆分到不同的Category里面，使类的结构更加清晰。\n系统API本身就有大量的例子，如UIViewController的：\n@interface UIViewController (UIContainerViewControllerCallbacks) // ... @end @interface UIViewController(UIViewControllerTransitioning) // ... @end 实现的时候还可以每个Category对应一个头文件和.m文件，类的结构更加清晰：\nXXXClass.h XXXClass.m XXXClass+XXXModuleA.h XXXClass+XXXModuleA.m XXXClass+XXXModuleB.h XXXClass+XXXModuleB.m 文件内代码组织 - #pragam mark、// MARK - 整理文件内的代码时，最好的手段就是用#pragam mark宏定义，Swift的话就是// MARK -。\nObjective-C冗长的语法注定了一个代码文件的行数不会少=。=，如果没有一定的规范，实在是难以迭代，所以最好就是开发前约定一种规则，对主要的类的实现都约定出一套规范，如UIViewController的子类、UIView的子类、UITableViewCell的子类等，然后开发人员按照这个规范写代码，这样才能使代码更好维护。\n如，UIViewController的子类实现时，可以按照如下顺序组织代码：\n@implementation XXXViewController // 内存相关 #pragma mark - Memory manager - (instancetype)init - (void)dealloc // 类生命周期相关的 #pragma mark - Life cycle (void)viewDidLoad // 类的Public方法 #pragma mark - Public methods // 所有的Actions #pragma mark - Actions - (void)didTapCancelButton:(UIButton *button) // 通知回调，具体可以细分 #pragma mark - Notifications - XXX // 系统的Delegate #pragma mark - UITableViewDelegate // 自定义类的Delegate #pragma mark - XXXDelegate // 自定义View、初始化等 #pragma mark - Custom views // 类私有方法 #pragma mark - Private methods @end 当然，具体的组织方法还要根据项目本身的架构、模式决定，上面的只是参考。\n进一步：创建项目专属文件模板\n既然定下了文件内的组织顺序，每次写代码手工输入也还是有点麻烦，所以更进一步的话就是创建自定义的Xcode文件模板，把不同类的#pragma mark顺序规则直接写到模板里面，以后开发人员创建文件时直接用项目专属的模板就可以了~\n具体的创建Xcode模板的步骤不在本文范畴=。=，不过倒是发现了一个Xcode插件，专门用来创建模板的：Stencil: A plugin for Xcode allowing you to easily create custom file templates，以及对应的文章：Introducing Stencil，还有一些教程：How to Create Custom Project Templates in Xcode 7，Creating Custom Xcode Templates，读者可自行阅读~\n最后 一切为了更加干净整洁的代码，“May the clean code be with you”\n参考 CocoaPods Carthage Xcode Groups vs. Folder References Framework vs Library (Static \u0026amp; Shared) in (Cocoa/OSX, Cocoa Touch/iOS) onevcat-如何打造一个让人愉快的框架 ","date":"2016-04-10T00:00:00Z","permalink":"/2016/04/10/summary-of-code-organization/","title":"总结一些iOS项目中组织代码的方法"},{"content":"前言 前段时间反复研读了蘑菇街 App 的组件化之路、蘑菇街 App 的组件化之路·续和iOS应用架构谈 组件化方案，然后又找到了其它一些研究组件化、模块化方案的文章，但是总觉得差点什么，所以还是决定从头开始思考。文章的标题起的好宽泛，感觉给自己挖了个深坑-。-，其实只是自己对组件化、模块化的一些看法、总结。\n为什么 先总结下为什么要大动干戈的对代码分模块、拆组件。\n代码量膨胀，不利于维护，更不利于新功能的开发\n现在随便开发一个App的代码行数都是数以万计的，如果不对代码做合理的拆分，那简直就是灾难性的，估计只有最初的开发人员知道如何维护修改，如果换人开发的话，难以下手，更不用说开发新功能了。\n不同业务代码耦合严重，难以多人合作，职责不分明\n多人一起开发时，如果代码结构、模块化的不好，就很难对不同业务划分出分界线，难以明确各自的职责，牵一发动全身，出了问题更是容易相互扯皮（这个时候只能说一句“怪我咯o(╯□╰)o”），更不用提合并代码时的冲突了。\n所以，合理的组织代码，划分模块、拆分组件是项目可以高效迭代的基础。\n疑问 那到底什么是模块化、组件化？查资料的时候一会儿模块，一会儿组件，有什么联系，有什么区别？有人说这只是叫法习惯问题，知道大概意思就好，不用咬文嚼字，但是总觉得没有个“定义”感觉不踏实，所以还是求助了万能的维基百科=。=\n模块化 维基百科的Modular programming的开头定义如下:\nModular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.\n接着，在Key aspects部分的开头也说了:\nWith modular programming, concerns are separated such that modules perform logically discrete functions, interacting through well-defined interfaces.\n可以总结为：模块化的目的在于将一个程序按照其功能做拆分，分成相互独立的模块，以便于每个模块只包含与其功能相关的内容，模块之间通过接口调用。\n当然，模块化编程的具体概念是包含了很多内容的，读者可以详细阅读下维基百科的定义。\n组件化 关于组件化，能找到的比较接近的就是维基百科的Component-based software engineering，其开头内容如下：\nComponent-based software engineering (CBSE), also known as component-based development (CBD), is a branch of software engineering that emphasizes the separation of concerns in respect of the wide-ranging functionality available throughout a given software system. It is a reuse-based approach to defining, implementing and composing loosely coupled independent components into systems.\n乍一看，这不是跟模块化Modular programming的定义很相似嘛=。= 的确，文中也提到组件化跟模块化是很类似的，都是主要为了对一个系统做拆分，比如文中提到：\nAll system processes are placed into separate components so that all of the data and functions inside each component are semantically related (just as with the contents of classes). Because of this principle, it is often said that components are modular and cohesive.\n同时，组件还具有其他属性，如可替代性(substitutable)，通过接口(interface)访问，可重用性(Reusability)等，读者可自行阅读。\n对比 难道模块化跟组件化真的是完全一样的？的确，很多时候两者的概念完全可以相互替换，在实践中更是经常混用。\n在求助谷歌，甚至阅读了大量的前端技术等其它技术领域的组件化、模块化的文章后，我觉得如果真要将它们两者做个对比，大概总结如下：\n模块化强调的是拆分，无论是从业务角度还是从架构、技术角度，模块化首先意味着将代码、数据等内容按照其职责不同分离，使其变得更加容易维护、迭代，使开发人员可以分而治之。 组件化则着重于可重用性，不管是界面上反复使用的用户头像按钮，还是处理数据的流程中的某个部件，只要可以被反复使用，并且进行了高度封装，只能通过接口访问，就可以称其为“组件”。 当然，并不是说模块就不能被复用，还是要根据实际情况来看，使系统更加容易维护，开发更加方便，才是最终目的。\n如何拆分 无论是模块化还是组件化，首先肯定是做拆分，但是如何拆分？怎么下手？依照什么标准？ 下面简单总结一些方法。\n横向拆分业务、功能模块 很多时候，一个完整的软件程序是同时为多种业务服务的，所有可以优先按照业务的不同，将整个系统进行拆分。\n如一个电商类型的App，就可以分出商品浏览模块、订单模块、购物车模块、消息模块、支付模块等。又如微信这种社交型应用，可以拆分出联系人模块、朋友圈模块、聊天模块、消息模块等。\n其实就是从用户使用的角度，按照功能的不同划分模块，当然，这种业务模块是要由各种技术模块作支撑的。\n纵向拆分技术、架构模块 如果脱离业务，只从技术角度来看，则可以尝试纵向对系统拆分模块。\n其实这里的纵向拆分跟对系统的架构做分层有点像=。=，现如今只要需要联网请求API的App都免不了有网络请求、数据缓存、数据加工处理、数据展示、反馈用户操作等行为，所有这些环节层层递进才能完成一个功能。\n当开始着手规划一个完整软件系统，或者说App时，就可以按照这些环节划分模块，纵向分层次的组合，搭建出一个以技术模块组成的简易系统架构图，方便后续的开发，如下图。\n大体上的技术模块划分好以后，就可以按照具体的需求，实现每个技术模块，乃至细分出更多的子模块，如缓存模块可能由键值对缓存（NSUserDefaults）、数据库缓存（SQLite、Realm）、图片缓存等子模块组成，根据具体情况而定。\n从界面入手，拆分可视化组件 现在再来看看如何从界面入手拆分可复用的组件。假如有如下布局的界面：\n很多时候，像界面里面的“搜索框”、“头像按钮”、“内容框”和显示提示用的“加载中”HUD，甚至整个内容的Cell，都是可能在很多地方出现的，而且本身的样式、功能比较集中。 如头像可能要支持点击跳转，头像图片圆角，内容框有特定的Padding和字体大小等，所以可以将这些界面上的元素“提”出来，单独封装成一个组件，供整个App复用。或者直接用第三方的组件，如图中的“加载中”HUD，就可以用SVProgressHUD、MBProgressHUD等开源库。\n其实这里的组件有种sunnyxx大大提到过的“Self-Manager”的味道=。=，组件本身负责自己的所有功能、样式，参考：iOS 开发中的 Self-Manager 模式。当然跟前端的组件化也挺像的，如React里面的component，样式、功能都封装到component里面，以便更好地解耦复用。\n从数据入手，拆分数据加工组件 再来看看从数据入手，拆分可复用的组件。假如有如下数据处理流程：\n其实大部分时候，拆分模块、组件都是以清晰的流程、逻辑为基础的，就如上图的过程，当流程清晰后，可以拆分复用的组件也就“出来了”。\n如从JSON数据实例化出对应的Entity对象，这个功能就是一个完整独立的组件，当然实际开发中会用Mantle、JSONModel等库实现。\n以此类推，校验、格式化日期（如“几秒钟前、几天前”）、多语言等环节，都可以独立成一个个的组件。\n当然，这里的组件一般是指能在多个模块使用的功能组件，如果只是在某个界面上才用的，倒不如放到ViewModel、Presenter等这些直接跟界面有关的类里面。\n小节 上面的几种方法比较适合不知道如何下手时使用=。=，真正的开发中，还是要根据实际情况考虑，情况也会复杂些。不过倒是可以总结几点原则：\n单一职责，意味着一个模块、一个组件只做一件事，绝不多做。 正交性，意思是不重复，一个模块跟另一个模块的职责是正交的，没有重叠，组件也是一样。 单向依赖，模块之间最多是单向的依赖，如果出现A依赖B，B也依赖A，那么要么是A、B应该属于一个模块，要么就是整体的拆分有问题。一个完整的软件系统的模块依赖应该是一张有向无环图。（当然这是最终理想=。=） 紧凑性，模块、组件对外暴露的接口、属性应该尽可能的少，接口的参数个数也要少。 面向接口，模块、组件对外提供服务时最好是面向接口的，以便后期可以灵活的变更实现。 最后 一切为了更加干净整洁的代码，“May the clean code be with you”\n参考 Component-based software engineering Modular programming ","date":"2016-03-29T00:00:00Z","image":"/assets/2016/03/29/modular-and-component-summary/modular_and_component_summary_1.jpg","permalink":"/2016/03/29/modular-and-component-summary/","title":"对组件化与模块化的思考与总结"},{"content":"更新 2017-02-27: 增加两种Alignment布局类型；增加点击前的回调判断；增加选中Tag上限 2016-12-25: 不再依赖UICollectionView，减少了位置刷新次数，提高性能；增加了alignment属性，可以靠左中右排列 2016-10-29: 增加Tag的阴影和ContentInset的设置，更新了Demo 2016-10-17: 增加了水平滑动；Autolayout适配；UITableViewCell例子等 2016-02-18: 修复了超长标签导致排版错误的Bug 前言 这段时间做项目的时候，总是需要显示一些“标签”样式的内容，但是又找不到用的顺手的库，所以琢磨了几天，自己实现了出来，就有了这个库：TTGTagCollectionView。如果只需要显示文字标签的话，直接使用TTGTextTagCollectionView，需要自己定义标签的话，就用TTGTagCollectionView，效果如下:\n支持五种布局排版：\nCocoaPods: pod \u0026quot;TTGTagCollectionView\u0026quot; Carthage: github \u0026quot;zekunyan/TTGTagCollectionView\u0026quot;\nGithub地址: https://github.com/zekunyan/TTGTagCollectionView\n只显示文字标签 - TTGTextTagCollectionView 基本使用 只显示文字标签的话，直接用TTGTextTagCollectionView类就可以了：\nTTGTextTagCollectionView *tagCollectionView = [[TTGTextTagCollectionView alloc] initWithFrame:CGRectMake(20, 20, 200, 200)]; [self.view addSubview:tagCollectionView]; [tagCollectionView addTags:@[@\u0026#34;TTG\u0026#34;, @\u0026#34;Tag\u0026#34;, @\u0026#34;collection\u0026#34;, @\u0026#34;view\u0026#34;]]; 接收点击事件 - 实现Delegate 如果想在标签被点击时得到通知，实现对应的Protocol即可，定义如下:\n@protocol TTGTextTagCollectionViewDelegate \u0026lt;NSObject\u0026gt; @optional // 是否可以点击某个Tag - (BOOL)textTagCollectionView:(TTGTextTagCollectionView *)textTagCollectionView canTapTag:(NSString *)tagText atIndex:(NSUInteger)index currentSelected:(BOOL)currentSelected; // 点击了某个Tag - (void)textTagCollectionView:(TTGTextTagCollectionView *)textTagCollectionView didTapTag:(NSString *)tagText atIndex:(NSUInteger)index selected:(BOOL)selected; // 内容大小更新 - (void)textTagCollectionView:(TTGTextTagCollectionView *)textTagCollectionView updateContentSize:(CGSize)contentSize; @end 自定义文字标签样式 如果想对标签的样式做定制，可以设置以下属性：\n// 标签是否可以被选中 @property (assign, nonatomic) BOOL enableTagSelection; // 字体 @property (strong, nonatomic) UIFont *tagTextFont; // 文字颜色 @property (strong, nonatomic) UIColor *tagTextColor; @property (strong, nonatomic) UIColor *tagSelectedTextColor; // 标签背景颜色 @property (strong, nonatomic) UIColor *tagBackgroundColor; @property (strong, nonatomic) UIColor *tagSelectedBackgroundColor; // 圆角值 @property (assign, nonatomic) CGFloat tagCornerRadius; @property (assign, nonatomic) CGFloat tagSelectedCornerRadius; // 边框 @property (assign, nonatomic) CGFloat tagBorderWidth; @property (assign, nonatomic) CGFloat tagSelectedBorderWidth; @property (strong, nonatomic) UIColor *tagBorderColor; @property (strong, nonatomic) UIColor *tagSelectedBorderColor; // 阴影 @property (nonatomic, copy) UIColor *tagShadowColor; // Default is [UIColor black] @property (nonatomic, assign) CGSize tagShadowOffset; // Default is (2, 2) @property (nonatomic, assign) CGFloat tagShadowRadius; // Default is 2f @property (nonatomic, assign) CGFloat tagShadowOpacity; // Default is 0.3f // 滚动方向，默认垂直方向 @property (nonatomic, assign) TTGTagCollectionScrollDirection scrollDirection; // 布局类型，默认靠左 @property (nonatomic, assign) TTGTagCollectionAlignment alignment; // 行数，默认0，不限制 @property (nonatomic, assign) NSUInteger numberOfLines; // 选中的数量限制 @property (nonatomic, assign) NSUInteger selectionLimit; // 标签宽高的扩展值，可以理解为padding @property (assign, nonatomic) CGSize extraSpace; // 水平间隔 @property (assign, nonatomic) CGFloat horizontalSpacing; // 行距 @property (assign, nonatomic) CGFloat verticalSpacing; // 整体的Inset @property (nonatomic, assign) UIEdgeInsets contentInset; // 所有标签大小 @property (nonatomic, assign, readonly) CGSize contentSize; // 是否显示滚动条 @property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; @property (nonatomic, assign) BOOL showsVerticalScrollIndicator; 增加、删除标签 // 增加一个标签 - (void)addTag:(NSString *)tag; - (void)addTags:(NSArray \u0026lt;NSString *\u0026gt; *)tags; /// 删除标签 - (void)removeTag:(NSString *)tag; - (void)removeTagAtIndex:(NSUInteger)index; - (void)removeAllTags; 在代码里控制标签的选中状态 - (void)setTagAtIndex:(NSUInteger)index selected:(BOOL)selected; 获取所有、选中、未选中标签 - (NSArray \u0026lt;NSString *\u0026gt; *)allTags; - (NSArray \u0026lt;NSString *\u0026gt; *)allSelectedTags; - (NSArray \u0026lt;NSString *\u0026gt; *)allNotSelectedTags; 重新加载 - Reload 用- (void)reload方法重新加载所有标签。\n显示自定义的标签控件View - TTGTagCollectionView 如果想自己定义标签View，如同时显示图像、文字、按钮，可以用TTGTagCollectionView类实现。\n基本要求 - 实现DataSource和Delegate DataSource的定义如下：\n@protocol TTGTagCollectionViewDataSource \u0026lt;NSObject\u0026gt; @required // 返回标签数量 - (NSUInteger)numberOfTagsInTagCollectionView:(TTGTagCollectionView *)tagCollectionView; // 返回对应标签View - (UIView *)tagCollectionView:(TTGTagCollectionView *)tagCollectionView tagViewForIndex:(NSUInteger)index; @end Delegate的定义如下:\n@protocol TTGTagCollectionViewDelegate \u0026lt;NSObject\u0026gt; @required // 必须实现：返回对应的标签Size - (CGSize)tagCollectionView:(TTGTagCollectionView *)tagCollectionView sizeForTagAtIndex:(NSUInteger)index; @optional // 是否可以点击Tag - (BOOL)tagCollectionView:(TTGTagCollectionView *)tagCollectionView shouldSelectTag:(UIView *)tagView atIndex:(NSUInteger)index; // 点击标签时回调 - (void)tagCollectionView:(TTGTagCollectionView *)tagCollectionView didSelectTag:(UIView *)tagView atIndex:(NSUInteger)index; // 所有标签总体Size更新 - (void)tagCollectionView:(TTGTagCollectionView *)tagCollectionView updateContentSize:(CGSize)contentSize; @end 跟UITableView的思路一致~\n设置标签的行距、间隔 // 滚动方向 @property (nonatomic, assign) TTGTagCollectionScrollDirection scrollDirection; // 布局方式，默认靠左 @property (nonatomic, assign) TTGTagCollectionAlignment alignment; // 行数，默认0，不限制 @property (nonatomic, assign) NSUInteger numberOfLines; // 水平间隔 @property (assign, nonatomic) CGFloat horizontalSpacing; // 行距 @property (assign, nonatomic) CGFloat verticalSpacing; // 整体Inset @property (nonatomic, assign) UIEdgeInsets contentInset; // 所有标签总体Size @property (nonatomic, assign, readonly) CGSize contentSize; // 是否显示滚动条 @property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; @property (nonatomic, assign) BOOL showsVerticalScrollIndicator; 重新加载 - Reload 用- (void)reload方法重新加载所有标签。\nEnd 2015最后一天~ 新年快乐~\n","date":"2015-12-31T00:00:00Z","image":"/assets/2015/12/31/TTGTagCollectionView/TTGTagCollectionView_screenshot.png","permalink":"/2015/12/31/TTGTagCollectionView/","title":"开源项目-TTGTagCollectionView"},{"content":"更新 2016-06-02: 感谢我不是蜡笔小新但是我有小白，发现了Case 2的最后两个cell展开动画问题，原来是tableView的estimatedRowHeight会跟展开动画冲突的缘故。 2015-12-23: 感谢XVXVXXX的PR,Case 1不再需要Fake Header View，直接用contentInset就好~ 2015-12-17：Case3的等间距，用UIStackView实现最优雅，但是无奈只有iOS9以上支持。兼容方案如OAStackView也可以，但是在UITableViewCell里面用，或者需要频繁的增减内部View的数量时，性能损耗有点厉害，会卡。所以，还是要看需求=。= 前言 第三篇来了。 依然是3个小例子，主要部分用Masonry手写代码实现，其它的约束在storyboard里面直接拖拽搭建。 至于为啥不用VFL，主要是因为它的“描述性”的写法很容易出错，没有补全、不好调试，写起来没有“代码”的感觉=。=。当然，这个仁者见仁智者见智~ 三个例子分别是：Parallax Header，动态变高度的UITableViewCell，以及两种方式实现等间距。原理其实都很简单，例子也都是平时积累起来的。\n前两篇：\n第一篇：有趣的Autolayout示例-Masonry实现 第二篇：有趣的Autolayout示例2-Masonry实现 第四篇：有趣的Autolayout示例4-Masonry实现 Github地址： https://github.com/zekunyan/AutolayoutExampleWithMasonry\nCase 1: Parallax Header Parallax翻译过来就是“视差”，我个人觉得就是一种“联动”的效果，在许多应用里面都能见到。当前这个例子，就是最简单的一种。\n原理 原理其实就是根据UITableView当前下拉的位移值，同步改变Parallax Header的高度，即NSLayoutConstraint的constant属性，对应到Masonry里面就是重新让约束equalTo()一次。\n主要步骤 主要的步骤如下：\n设置UITableView背景透明。 在UITableView正下方放置一个UIImageView，作为我们的Parallax Header，设置contentMode为UIViewContentModeScaleAspectFill，并加上上左右的固定约束，使其与UITableView对其，然后加上一个固定高度的约束，并在代码里面保存。 设置UITableView的contentInset跟Parallax Header等高，使UITableView的头部“撑开”，让后面的Parallax Header露出来。 在代码里面监听UITableView的contentOffset属性，当y小于0时，增加Parallax Header高度，使其产生联动效果。 约束示意图如下： 代码 创建Parallax Header的代码如下：\n_parallaxHeaderView = [UIImageView new]; // 把Parallax Header放在UITableView的下面 [self.view insertSubview:_parallaxHeaderView belowSubview:_tableView]; // 设置contentMode _parallaxHeaderView.contentMode = UIViewContentModeScaleAspectFill; _parallaxHeaderView.image = [UIImage imageNamed:@\u0026#34;parallax_header_back\u0026#34;]; // 添加约束 [_parallaxHeaderView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.and.right.equalTo(self.view); make.top.equalTo(self.mas_topLayoutGuideBottom); // 保存高度约束 _parallaxHeaderHeightConstraint = make.height.equalTo(@(ParallaxHeaderHeight)); }]; 设置UITableView的contentInset\n_tableView.contentInset = UIEdgeInsetsMake(ParallaxHeaderHeight, 0, 0, 0); 监听contentOffset属性的两种方法 方法1：直接实现scrollViewDidScroll: 这种方法应该是最直接的：\n- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView.contentOffset.y \u0026lt; 0) { // 增加Parallax Header对应的高度，y是负数，所以减去 _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - scrollView.contentOffset.y)); } else { _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight)); } } 方法2：KVO监听contentOffset变化 用KVO的好处就是不用要求当前类实现UITableView的delegate，对于代码的拆分有好处。\n增加KVO：\n[_tableView addObserver:self forKeyPath:@\u0026#34;contentOffset\u0026#34; options:NSKeyValueObservingOptionNew context:nil]; 实现监听:\n- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary\u0026lt;NSString *,id\u0026gt; *)change context:(void *)context { if ([keyPath isEqualToString:@\u0026#34;contentOffset\u0026#34;]) { // 取出contentOffset值 CGPoint contentOffset = ((NSValue *)change[NSKeyValueChangeNewKey]).CGPointValue; // 改变高度 if (contentOffset.y \u0026lt; 0) { _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - contentOffset.y)); } else { _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight)); } } } 最后别忘了取消KVO:\n- (void)dealloc { [_tableView removeObserver:self forKeyPath:@\u0026#34;contentOffset\u0026#34;]; } 小节 NSLayoutConstraint的constant属性非常有用，既可以做动画，也可以方便的调整现有布局，大家多多挖掘哈~\nCase 2: 动态变高度的UITableViewCell 嗯，又是UITableViewCell=。= 只不过这次的是“动态改变高度”，就是类似于微信朋友圈里面的“全文”那种效果。\n单纯的不定高UITableViewCell不是本例子的重点，详细请看有趣的Autolayout示例2-Masonry实现里面的Case1。\n先说一点我觉得在代码设计上比较重要的地方：Cell只负责显示内容，不应该保存具体的状态信息。\n我们都知道，UITableViewCell是会被重用的，也就是说，不能保证UITableView里面的哪一行一定由哪一个UITableViewCell实例展示。 动态展开、收回Cell的时候，我们需要一个BOOL变量，用于保存当前Cell的展开、收回的状态。这个BOOL变量就是所谓的“状态”，这个状态应该保存在当前Cell的数据里面，如Entity、ViewModel里面。对Cell填充数据的时候，再根据这个“状态”，修改对应的约束。\n主要步骤 先看看大致的步骤：\n通过点击的Cell找到对应的数据Entity。 改变这一行数据Entity用于保存状态的BOOL变量的值。 让UITableView刷新这一行。 刷新的时候，Cell根据这个BOOL变量重新调整约束、填充数据，得到新的高度。 最后就是Cell的高度变化。（此时的这一行的Cell实例并不一定是之前的那个实例） 布局 为了尽量简单，例子里面的Cell只有三个子控件，第一个UILabel是标题等调试信息，第二个UILabel用来显示多行文本，最后一个UIButton用来切换展开、收回的状态。大致的布局如下：\n布局的代码 标题UIlabel\n_titleLabel = [UILabel new]; [self.contentView addSubview:_titleLabel]; [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@21); make.left.and.right.and.top.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8)); }]; 底部“More”按钮\n_moreButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_moreButton setTitle:@\u0026#34;More\u0026#34; forState:UIControlStateNormal]; [_moreButton addTarget:self action:@selector(switchExpandedState:) forControlEvents:UIControlEventTouchUpInside]; [self.contentView addSubview:_moreButton]; [_moreButton mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@32); make.left.and.right.and.bottom.equalTo(self.contentView); }]; 正文UIlabel\nCGFloat preferredMaxWidth = [UIScreen mainScreen].bounds.size.width - 16; // Content - 多行 _contentLabel = [UILabel new]; _contentLabel.numberOfLines = 0; _contentLabel.lineBreakMode = NSLineBreakByCharWrapping; _contentLabel.clipsToBounds = YES; _contentLabel.preferredMaxLayoutWidth = preferredMaxWidth; // 多行时必须设置 [self.contentView addSubview:_contentLabel]; [_contentLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.and.right.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8)); make.top.equalTo(_titleLabel.mas_bottom).with.offset(4); make.bottom.equalTo(_moreButton.mas_top).with.offset(-4); // 先加上高度的限制 _contentHeightConstraint = make.height.equalTo(@64).with.priorityHigh(); // 优先级只设置成High,比正常的高度约束低一些,防止冲突 }]; 为什么要加正文UIlabel高度约束 有趣的Autolayout示例2-Masonry实现里面的Case1也讲过，获取Cell的高度的方法是systemLayoutSizeFittingSize:，如果不对正文UILabel加上高度约束，获取的高度就是根据正文的内容计算出来的，这与之前的例子里面一致。\n为了使高度固定，就需要加上一个高度约束，使得systemLayoutSizeFittingSize:计算时按照这个约束去计算。\n为什么正文UILabel的高度约束的优先级要调整为High 在UITableView刷新时，会先计算高度，即先调用tableView: heightForRowAtIndexPath:方法，如果高度约束为默认的1000最高的话，会产生冲突。\n因为在计算的时候，我们的高度是由一个“template cell”填充内容后计算得来，这个时候的高度已经是展开以后的高度，当前的Cell还来不及调整约束（甚至不会调整，如果只用beginUpdates和endUpdates更新的话，Cell不会reload），所以降低这个高度约束的优先级，去掉冲突。\n使用install和uninstall控制约束 为了能得正确高度，Cell需要根据具体的数据、状态更新约束。 这里可以使用install和uninstall来控制正文UILabel高度约束是否生效。在填充Cell的数据时，就可以根据状态BOOL值来选择调用：\n- (void)setEntity:(Case8DataEntity *)entity indexPath:(NSIndexPath *)indexPath { // 设置数据... // 改变约束 if (_entity.expanded) { [_contentHeightConstraint uninstall]; } else { [_contentHeightConstraint install]; } } 创建Delegate，使得Cell的事件得以回传到ViewController 在点击Cell的“More”按钮时，需要改变当前的展开收回状态BOOL值，还需要让UITableView刷新。 直接在Cell里面修改Entity数据，或者持有UITableView实例都是不恰当的，这个时候可以用Delegate模式实现。\nDelegate如下：\n@protocol Case8CellDelegate \u0026lt;NSObject\u0026gt; - (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index; @end 然后ViewController实现这个Protocol\n- (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index { // 取出对应数据 Case8DataEntity *case8DataEntity = _data[(NSUInteger) index.row]; // 修改状态 case8DataEntity.expanded = !case8DataEntity.expanded; // 切换展开还是收回 case8DataEntity.cellHeight = 0; // 重置高度缓存 // 刷新UITableView } Cell保存ViewController这个delegate，然后在按钮点击时回调\n// Cellb保存delegate，注意weak @property (weak, nonatomic) id \u0026lt;Case8CellDelegate\u0026gt; delegate; // Cell的“More”按钮点击 - (void)switchExpandedState:(UIButton *)button { [_delegate case8Cell:self switchExpandedStateWithIndexPath:_indexPath]; } 刷新的方式 UITableView的刷新可以用以下几种方法：\n1. reloadData 用reloadData刷新，其实就是把所有Cell都刷新了一次，代价有点大，不推荐。\n2. reloadRowsAtIndexPaths:withRowAnimation: 这个方法的好处就是可以指定要刷新的哪几行，而且可以指定刷新时的动画形式，一般来说用UITableViewRowAnimationFade就不错。 刷新的时候，tableView:cellForRowAtIndexPath:会被调用，原来的Cell实例会被替换。\n3. beginUpdates和endUpdates 这两个方法一般都是成对使用的，在中间可以执行插入、删除等调整Cell的操作，改变Cell的高度也可以用它。 不过要注意的是，这两个方法并不会重新加载Cell，只是单纯的改变了高度，所以如果Cell原来的约束里面有高度约束这种，而又保持默认的优先级，就会产生约束冲突。\n从效果上来讲，我个人觉得用reloadRowsAtIndexPaths:withRowAnimation:会更好一些~\n小节 想用好Autolayout不容易啊，要仔细研究UITableView的机制=。=\nCase 3: 两种方式实现等间距 等间距，也就是View之间的X或Y轴上的坐标等差，在这里我只举出水平方向上的等间距，垂直方向上一个道理。\n方法1：利用透明等宽的占位View填充空白处，实现等间距 步骤很简单，就是循环创建真正要展示的View和占位View，布局如下：\n说明一下：\n占位View的宽度不能定死，这样外部的父级View宽度变化时，内部的View仍然可以保持等间距。 既然占位View宽度不定，总得有个宽度的参照，这个参照就是其它的占位View，也就是说，要给占位View加上两两宽度相等的约束。 代码如下:\n// 先创建第一个占位View UIView *lastSpaceView = [UIView new]; lastSpaceView.backgroundColor = [UIColor greenColor]; // 用绿色标出 [_containerView1 addSubview:lastSpaceView]; // 添加上左下三个约束 [lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.and.top.and.bottom.equalTo(_containerView1); }]; // 循环创建 for (NSUInteger i = 0; i \u0026lt; ITEM_COUNT; i++) { // 创建ItemView，即真正显示内容的View UIView *itemView = [self getItemViewWithIndex:i]; [_containerView1 addSubview:itemView]; // 固定宽高，左边、垂直方向中心与上一个占位View对齐。 [itemView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.and.width.equalTo(@(ITEM_SIZE)); make.left.equalTo(lastSpaceView.mas_right); make.centerY.equalTo(_containerView1.mas_centerY); }]; // 创建下一个占位View UIView *spaceView = [UIView new]; spaceView.backgroundColor = [UIColor greenColor]; [_containerView1 addSubview:spaceView]; // 左边与当前ItemView对齐，上下与边界对齐，宽度与上一个占位View相等！ // 但是右边的约束不能加，因为要留给下一次循环与下一个ItemView的左边界添加 [spaceView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(itemView.mas_right).with.priorityHigh(); // 降低优先级，防止宽度不够出现约束冲突 make.top.and.bottom.equalTo(_containerView1); make.width.equalTo(lastSpaceView.mas_width); }]; // 更新 lastSpaceView = spaceView; } // 为最后一个占位View添加右边约束 [lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(_containerView1.mas_right); }]; 像这种重复添加View和约束，合理使用变量保存“上一次循环”创建的占位View，就可以大大简化代码，而且可以任意调整数量~\n方法2：直接按比例设置multiplier 等间距，其实就是按比例，再进一步就是x坐标是按比例的。 延伸到View上，可以理解为centerX的值与父级View的宽度按比例增减。\n但是，读者可以尝试一下，直接设置一个View的边界、位置属性，如centerX，等于其父级View的宽度是会报错的。\n难道就没有办法了？当然不是。\n始终在坐标系上考虑约束\n在上一篇有趣的Autolayout示例2-Masonry实现的开头我也提到过，Autolayout最终都是体现在坐标系上，一切都会按照viewA-attribute = viewB-attribute * multiplier + constant这种公式去计算，既然centerX不能跟父级的Width宽度一起加约束，那就换一个，如父级的右边界，父级View的右边界在父级本身的参照系下的Y坐标值不就等于其宽度吗~\n所以，可以按照如下方式加约束：\n对应的代码也会异常简单：\n// 循环创建 for (NSUInteger i = 0; i \u0026lt; ITEM_COUNT; i++) { UIView *itemView = [self getItemViewWithIndex:i]; [_containerView2 addSubview:itemView]; [itemView mas_makeConstraints:^(MASConstraintMaker *make) { // 宽高一定 make.width.and.height.equalTo(@(ITEM_SIZE)); // 确定Y坐标 make.centerY.equalTo(_containerView2.mas_centerY); // 确定X坐标，注意分子分母都要加1 make.centerX.equalTo(_containerView2.mas_right).multipliedBy(((CGFloat)i + 1) / ((CGFloat)ITEM_COUNT + 1)); }]; } 小节 很多时候，灵活的使用multiplier能大大简化开发~\n总结 三个例子说完了，你有啥收获呢~？:-D\n","date":"2015-12-14T00:00:00Z","image":"/assets/2015/12/14/autolayout-example-with-masonry3/blog_autolayout_example_with_masonry3_1.gif","permalink":"/2015/12/14/autolayout-example-with-masonry3/","title":"有趣的Autolayout示例3-Masonry实现"},{"content":"前言 前段时间在Dribbble上发现了一个Rating控件的演示动画，控件以Emoji表情为基础，结合了上下滑动手势，正好最近正在深入学习iOS动画、绘图相关的知识，就尝试着用UIBezierPath实现了出来。本文就是TTGEmojiRate的实现过程。\nGithub: https://github.com/zekunyan/TTGEmojiRate\n分析 先看看原本的效果：Rating Version A - Hoang Nguyen\n可以看出来，主要的特点如下：\n可以上下拖动，改变Emoji表情嘴的弧度。 拖动的时候Rate的值也会随之变化，从0到5，并且跟表情的“喜怒”相对应。 颜色也会变化，从绿色到蓝色再到红色，也对应表情的“喜怒”。 实际实现的时候，增加了眼睛元素，并且增强了自定义，如颜色的变化范围、线条的粗细等都可以设定，基本的思路还是不变的。\n实现 思路 开始写代码之前，先理理思路。\n拖动的时候，直接影响的应该是Rate值，然后在Rate值改变的时候刷新整个控件，刷新的时候重绘。重绘的时候，嘴、眼睛的弧度，颜色的值都要根据Rate值重新计算，如下图：\n拖动改变Rate值 这个还是很容易实现的，直接重写UIView的touch相关的三个方法，在里面记录拖动在Y轴上的变化值，然后映射到Rate值上就可以了。\n先声明一个CGPoint属性，用来保存手指按下时的点位置：\nprivate var touchPoint: CGPoint? = nil 在手指移动的时候，在touchesMoved方法里面计算当前点跟上一次触摸点的Y轴上的差值，然后映射到Rate值上。\npublic override func touchesMoved(touches: Set\u0026lt;UITouch\u0026gt;, withEvent event: UIEvent?) { // 获取当前触摸点 let currentPoint = touches.first?.locationInView(self) // 改变Rate值 rateValue = rateValue + Float((currentPoint!.y - touchPoint!.y) / CGRectGetHeight(self.bounds) * rateDragSensitivity) // 保存当前触摸点 touchPoint = currentPoint } 注意：\n计算Y轴差值的时候，除以了当前控件的高度，这是为了保证Rate值按比例增减。 增加了一个rateDragSensitivity属性，用来调节改变Rate值的“灵敏度” UIBezierPath - 贝塞尔曲线 控件的主要内容都是用UIBezierPath绘制出来的。网上关于UIBezierPath的讲解很多，在这里就不详细说了。\n简单来说，UIBezierPath用来绘制矢量路径，是一种参数曲线，在使用的时候，只需要先设定好锚点、控制点，系统就可以根据贝塞尔曲线的算法，绘制出对应的线，并且保证锚点和对应的控制点的连线与曲线相切。\n这里有一个演示绘制贝塞尔曲线过程的网站：Bézier curve\n脸 脸是最简单的，就是一个圆，直接用一个方法就可以：\nprivate func drawFaceWithRect(rect: CGRect) { let facePath = UIBezierPath(ovalInRect: rect) rateColor.setStroke() facePath.lineWidth = rateLineWidth // 线粗细 facePath.stroke() } 实际实现的时候可以加上Margin，防止线画到View的边界之外。\n嘴、眼睛 先看看UIBezierPath提供的可以用来绘制曲线的方法： addCurveToPoint(_:controlPoint1:controlPoint2:)和addQuadCurveToPoint(_:controlPoint:)，如下图:\n直观上来讲，嘴、眼睛的绘制跟addQuadCurveToPoint方法绘制的效果基本一致，但是这样的效果没法调整，因为只能控制唯一的一个控制点，所以还是要用addCurveToPoint方法，对称的绘制两条曲线，拼接起来，如下图：\n这样的话，就可以通过调整两个控制点，来控制嘴、眼睛的弯曲宽度、形状。\n以绘制嘴为例：\nprivate func drawMouthWithRect(rect: CGRect) { let width = CGRectGetWidth(rect) let height = CGRectGetWidth(rect) // 左端点 let leftPoint = CGPointMake( width * (1 - rateMouthWidth) / 2, height * (1 - rateMouthVerticalPosition)) // 右端点 let rightPoint = CGPointMake( width - leftPoint.x, leftPoint.y) // 中间点 - Y值根据当前的Rate值计算，0.3为系数 let centerPoint = CGPointMake( width / 2, leftPoint.y + height * 0.3 * (CGFloat(rateValue) - 2.5) / 5) // 控制点跟中间点在X轴上的距离 let halfLipWidth = width * rateMouthWidth * rateLipWidth / 2 // 创建贝塞尔曲线 let mouthPath = UIBezierPath() // 移动到起始点 mouthPath.moveToPoint(leftPoint) // 添加左半边曲线路径 mouthPath.addCurveToPoint( centerPoint, controlPoint1: leftPoint, controlPoint2: CGPointMake(centerPoint.x - halfLipWidth, centerPoint.y)) // 添加右半边曲线路径 mouthPath.addCurveToPoint( rightPoint, controlPoint1: CGPointMake(centerPoint.x + halfLipWidth, centerPoint.y), controlPoint2: rightPoint) // 设定样式 mouthPath.lineCapStyle = CGLineCap.Round; rateColor.setStroke() mouthPath.lineWidth = rateLineWidth mouthPath.stroke() } 说明：\n所有的距离、坐标都是根据当前控件的大小计算出来的。 rateMouthWidth为嘴的宽度与整个控件宽度的比值，即相对值。 rateMouthVerticalPosition为嘴的左右两个端点的Y轴坐标值，也为相对值。 rateLipWidth为中心点的两个控制点的距离与嘴宽度的比值，也是相对值。 眼睛的绘制跟嘴原理一致，就不再说明。\n颜色的渐变 Dribbble的演示中，控件的线条颜色也是会变化的，从红色到蓝色再到绿色，是连续变化的。这个时候用常见的RGB色彩模式是不好控制的，效果也不好。\n所以这个时候要用HSB色彩模式\nHSB 色彩模式是基于人眼的一种颜色模式。是普及型设计软件中常见的色彩模式，其中H代表色相；S代表饱和度；B代表亮度。- 百度百科\n对应到UIColor类，就是下面两个方法：\n// 创建UIColor init(hue hue: CGFloat, saturation saturation: CGFloat, brightness brightness: CGFloat, alpha alpha: CGFloat) // 获取HSB值，注意参数 func getHue(_ hue: UnsafeMutablePointer\u0026lt;CGFloat\u0026gt;, saturation saturation: UnsafeMutablePointer\u0026lt;CGFloat\u0026gt;, brightness brightness: UnsafeMutablePointer\u0026lt;CGFloat\u0026gt;, alpha alpha: UnsafeMutablePointer\u0026lt;CGFloat\u0026gt;) -\u0026gt; Bool 实现的时候，为了增加可定制性，控件颜色的变化范围是可以设置的，用以下属性保存：\npublic var rateColorRange: (from: UIColor, to: UIColor) 刷新时，就可以根据当前的Rate值，重新计算颜色的HSB和alpha值：\nlet rate: CGFloat = CGFloat(rateValue / 5) // Rate值归一化 self.rateColor = UIColor.init( hue: hueFrom + hueDelta * rate, // 色相 saturation: saturationFrom + saturationDelta * rate, // 饱和度 brightness: brightnessFrom + brightnessDelta * rate, // 亮度 alpha: alphaFrom + alphaDelta * rate // 透明度 ) 说明：\n所有的颜色参数都是根据Rate值做线性增减。 xxxFrom、xxxDelta分别指HSB和alpha的起始值与变化范围，在设置rateColorRange时计算保存下来。 这样，颜色就能做到跟Rate值做连续的线性变化。\n善于使用didSet 实现控件的时候，对外暴露了很多属性，如线的宽度rateLineWidth、嘴的宽度rateMouthWidth等。为了对这些属性做校验，并且在设置后刷新控件，就要用到didSet。\ndidSet在Swift里面，跟类的属性是一一绑定的，在对属性赋值后会被调用。\n控件的大部分属性都做了校验、刷新，如下：\n/// Mouth width. From 0.2 to 0.7. @IBInspectable public var rateMouthWidth: CGFloat = 0.6 { didSet { // 判断上限 if rateMouthWidth \u0026gt; 0.7 { rateMouthWidth = 0.7 } // 判断下限 if rateMouthWidth \u0026lt; 0.2 { rateMouthWidth = 0.2 } // 刷新、重绘 self.setNeedsDisplay() } } @IBDesignable、@IBInspectable 为了能在XIB、StoryBoard里面使用、编辑控件，就要用到@IBDesignable和@IBInspectable这两个关键字。\n在类的前面加上@IBDesignable关键字，使IB可以预览控件：\n@IBDesignable public class EmojiRateView: UIView { // ... } 在属性前面加上@IBInspectable，就可以在IB里面编辑属性，实时预览：\n@IBInspectable public var rateLineWidth: CGFloat = 14 { // ... } 详细的使用可以参考NSHipster上的文章：IBInspectable / IBDesignable\n最后，在IB里面就是下面这样： By the way =。= 属性名字太长，在IB里面显示不完整，咋办。。。\n回调 拖动改变Rate值的时候，肯定要有回调，如下定义：\npublic var rateValueChangeCallback: ((newRateValue: Float) -\u0026gt; Void)? = nil 在rateValue的didSet里面回调：\n@IBInspectable public var rateValue: Float = 2.5 { didSet { // ... // 回调 self.rateValueChangeCallback?(newRateValue: rateValue) } } 总结 看似简单的一个Rating控件，从构思到实现，再到完善，一点一点朝着完美去做，收获不少~\n最后，Dribbble是个好地方，贝塞尔曲线好强大，XCode 7.1写Swift还是有点卡=。=\n以上。\n参考 维基百科 - 贝塞尔曲线 Drawing Shapes Using Bézier Paths HSB色彩模式 IBInspectable / IBDesignable ","date":"2015-10-25T00:00:00Z","image":"/assets/2015/10/25/ttgemojirate-lib/TTGEmojiRate_screenshot2.gif","permalink":"/2015/10/25/ttgemojirate-lib/","title":"Swift开源项目: TTGEmojiRate的实现"},{"content":"更新 2016-12-25: 增加多行文本的支持，增加阴影 2016-10-20: 增加ContentInset，也就是Padding的属性；增加自定义内容View; 版本1.3.0 2016-10-17: 支持Swift3；增加的顶部Top位置动画；增加同时两个Action按钮；增加带图片icon显示 2016-03-25: 版本更新到了0.5.1，补充些说明、示例 TTGSnackbar 用Swift写了个简单的控件库：TTGSnackbar，模仿Android的Snackbar。目前更新到了版本1.3.0。支持多种动画，增强了自定义。\nGithub: https://github.com/zekunyan/TTGSnackbar\nGif\n安装 需要Swift3, Xcode8, iOS 8+\nCocoaPods platform :ios, \u0026#39;8.0\u0026#39; use_frameworks! pod \u0026#34;TTGSnackbar\u0026#34; Carthage github \u0026#34;zekunyan/TTGSnackbar\u0026#34; 简单的使用案例 只显示文字 let snackbar = TTGSnackbar.init(message: \u0026#34;Message\u0026#34;, duration: .Short) snackbar.show() 显示文字和一个动作按钮 let snackbar = TTGSnackbar.init(message: \u0026#34;Message\u0026#34;, duration: .Middle, actionText: \u0026#34;Action\u0026#34;) { (snackbar) -\u0026gt; Void in NSLog(\u0026#34;Click action!\u0026#34;) } snackbar.show() 显示文字和一个耗时操作按钮 let snackbar = TTGSnackbar.init(message: \u0026#34;Message\u0026#34;, duration: .Forever, actionText: \u0026#34;Action\u0026#34;) { (snackbar) -\u0026gt; Void in NSLog(\u0026#34;Click action!\u0026#34;) // Dismiss manually after 3 seconds dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -\u0026gt; Void in snackbar.dismiss() } } snackbar.show() 显示文字和两个动作按钮 let snackbar: TTGSnackbar = TTGSnackbar.init(message: \u0026#34;Two actions !\u0026#34;, duration: .Long) // Action 1 snackbar.actionText = \u0026#34;Yes\u0026#34; snackbar.actionTextColor = UIColor.greenColor() snackbar.actionBlock = { (snackbar) in NSLog(\u0026#34;Click Yes !\u0026#34;) } // Action 2 snackbar.secondActionText = \u0026#34;No\u0026#34; snackbar.secondActionTextColor = UIColor.yellowColor() snackbar.secondActionBlock = { (snackbar) in NSLog(\u0026#34;Click No !\u0026#34;) } snackbar.show() 显示文字和图片ICON let snackbar: TTGSnackbar = TTGSnackbar.init(message: \u0026#34;Two actions !\u0026#34;, duration: .Long) // Add icon image snackbar.icon = UIImage.init(named: \u0026#34;emoji_cool_small\u0026#34;) snackbar.show() 自定义 可以自定义文字的字体、颜色，动作按钮的字体、颜色，回调等等，详细的请看Github的README说明。😊\n总之 一时兴起写了个简单的库，后来发现居然有人提交PR、issue，不敢怠慢啊，也就坚持了下来，断断续续更新了10个小版本了=。=（2016年3月25号，25周岁前一天-_-|||）\n","date":"2015-10-09T00:00:00Z","image":"/assets/2015/10/09/ttgsnackbar-lib/TTGSnackbar_screen_shot.png","permalink":"/2015/10/09/ttgsnackbar-lib/","title":"Swift写的库-TTGSnackbar"},{"content":"前言 Masonry写的Autolayout示例又来了，仍然是三个小例子，分别是变高度的UITableViewCell、topLayoutGuide与bottomLayoutGuide，还有自定义的baseline，外加两个基本的知识点讲解，说不上“有趣”=。=，比较基础，写了很多，各位随意看看吧~\n第一篇: 有趣的Autolayout示例-Masonry实现 第三篇: 有趣的Autolayout示例3-Masonry实现 第四篇: 有趣的Autolayout示例4-Masonry实现 Github地址： https://github.com/zekunyan/AutolayoutExampleWithMasonry\n知识点 先讲讲两个知识点，很基础，但是很容易被忽略。\n坐标系、top、right、offset 先看看Masonry的Github主页的示例代码：\nUIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10); [view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(superview.mas_top).with.offset(padding.top); make.left.equalTo(superview.mas_left).with.offset(padding.left); make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom); make.right.equalTo(superview.mas_right).with.offset(-padding.right); }]; 代码的意思很简单，就是view1的上下左右边距为padding对应的值。\n但是，为什么bottom、right的offset是负数呢？\n其实无论是Autolayout还是直接写frame，最终的结果都是要把我们的控件按照正确的位置绘制在屏幕上，也就是说，在一个统一的坐标系下，如下：\n而在Masonry里面，offset只是做了“加法”运算，举个例，上面的：\nmake.top.equalTo(superview.mas_top).with.offset(padding.top); 其实等于下面的式子：\nview1.top = superview.top + padding.top; 转换到坐标系里面，即是：\nview1顶部的y坐标 = superview顶部的y坐标 + padding.top; 所以，如果我们想view1的bottom距底部间距为10，按照offset的“加法运算”，应该是下面这样：\nview1底部的y坐标 = superview底部的y坐标 + (-10); 所以，代码里面的bottom的offset是负数。right也是一个道理。\n总的来说，就是布局的时候，始终要在坐标系下考虑。\n约束的“等价”性 语文不好，还是用公式说明吧=。= 先看看Autolayout的基本公式：\nviewA-attribute = viewB-attribute * multiplier + constant 这个公式，跟下面的是等价的：\nviewB-attribute = (viewA-attribute - constant) / multiplier 这个转换是如此的简单，小学生都会=。=，只是为了说明，我们在设置约束的时候，既可以从ViewA的角度考虑，也可以从ViewB的角度，两者完全等价！。\n说白了就是：“ViewA跟ViewB相距10”和“ViewB跟ViewA相距10”是一样的，如下两段代码，效果是一样的（注意正负数）：\n// View2的顶部距离View1的底部10 [view2 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(view1.mas_bottom).with.offset(10); }]; // View1的底部距离View2的顶部10 [view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(view2.mas_top).with.offset(-10); }]; 更进一步说，就是约束只是两个View之间的关系，对于系统来说，ViewA和ViewB的地位是平等的，我们设置约束的时候，没有主次之分。\n所以，我们在设置约束的时候，要从“整体”、“宏观”上考虑，更好地把握布局，避免重复约束。\nCase 1: 变高UITableViewCell 变高的UITableViewCell，这是个永恒的话题=。=\n不用Autolayout的话，计算Cell的高度的时候，就只能用sizeThatFits等方法，加上各种“魔鬼”数据的margin、padding来手动拼凑出Cell的高度。这种方法非常耗时，并且难以调试。\n有了Autolayout，就再也不用手动算高度了~\nUITableViewCell 先看看Cell的约束。\n“自我约束”的Cell 既然要能让系统自己计算出Cell的高度，我们在设置约束的时候，就要让约束整体是“完整”、“自我约束”的。（这个很难用语言描述。。。）Cell里面的每一个View的大小、位置，都可以从约束中得到体现，而Cell的整体大小，也是从子View的约束综合计算得出的。\n如下面的Cell：\n左上角的图片固定大小。 标题的Label只显示一行，固定高度。 内容的Label根据内容决定高度。 两个Label宽度整体随着Cell的宽度变化。 约束的设定如下：\n关键点:\n内容Label的bottom和Cell的contentView的约束不可以省，因为cell的高度要由内部的约束决定，所以上下左右的约束一个不能少。 内容Label的高度随着内容变化，即cell的高度随内容变化，这个时候可以设置Label的ContentHugging的优先级最高。 UILabel的preferredMaxLayoutWidth Autolayout下，UILabel在多行显示时，有个很“坑”的属性需要设定，就是preferredMaxLayoutWidth。\n定义如下:\nThis property affects the size of the label when layout constraints are applied to it. During layout, if the text extends beyond the width specified by this property, the additional text is flowed to one or more new lines, thereby increasing the height of the label.\n如果我们要使用Autolayout自动计算多行UILabel的高度，这个属性就必须在运行时指定，要不然系统计算不出UILabel的宽度，例如：\n// 计算UILabel的preferredMaxLayoutWidth值，多行时必须设置这个值，否则系统无法决定Label的宽度 label.preferredMaxWidth = [UIScreen mainScreen].bounds.size.width - margin - padding; 手动计算宽度，感觉回到了没有Autolayout的时代=。=\nCell的关键代码 // Avatar - 头像 _avatarImageView = [UIImageView new]; [self.contentView addSubview:_avatarImageView]; [_avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.and.height.equalTo(@44); // 宽高固定 make.left.and.top.equalTo(self.contentView).with.offset(4); }]; // Title - 单行 _titleLabel = [UILabel new]; [self.contentView addSubview:_titleLabel]; [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@22); // 宽高固定 make.top.equalTo(self.contentView).with.offset(4); make.left.equalTo(_avatarImageView.mas_right).with.offset(4); make.right.equalTo(self.contentView).with.offset(-4); }]; // 计算UILabel的preferredMaxLayoutWidth值，多行时必须设置这个值，否则系统无法决定Label的宽度 CGFloat preferredMaxWidth = [UIScreen mainScreen].bounds.size.width - (16 + 4) * 2 - 44 - 4; // Content - 多行 _contentLabel = [UILabel new]; _contentLabel.numberOfLines = 0; _contentLabel.preferredMaxLayoutWidth = preferredMaxWidth; // 多行时必须设置 [self.contentView addSubview:_contentLabel]; [_contentLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(_titleLabel.mas_bottom).with.offset(4); make.left.equalTo(_avatarImageView.mas_right).with.offset(4); make.right.equalTo(self.contentView).with.offset(-4); make.bottom.equalTo(self.contentView).with.offset(-4); }]; // 设置高度的Content Hugging [_contentLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; UITableView 再看看UITableView。\n用systemLayoutSizeFittingSize:获取Cell的高度 在设定好Cell的约束以后，就可以用systemLayoutSizeFittingSize:方法获取Cell的实际高度，它的参数可以设定为两个系统常量，如下：\nUILayoutFittingCompressedSize: 返回合适的最小大小。 UILayoutFittingExpandedSize: 返回合适的最大大小。 模板Cell 为了在- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法中计算Cell的高度，我们需要一个专门用于计算高度的Cell实例，可以说算是Cell的“模板”。一般来说，这个实例可以设置成函数的static变量，并只在第一次使用时初始化一次。\n简单缓存高度 为了避免每次滑动时计算高度，可以将Cell的高度缓存下来。如，保存在每一行对应的数据Model(Entity)中，例如：\n@interface Entity : NSObject // Data @property (copy, nonatomic) NSString *title; // ... // Cache height @property (assign, nonatomic) CGFloat cellHeight; @end 每次要获取高度时，就可以先检查一下是否有缓存，减少计算量。\n设置estimatedRowHeight以减少首次显示的计算量 默认情况下，首次显示之前，系统都会一次性全部计算出所有Cell的高度，这简直不能忍啊！要是有10000行，那岂不是要卡死=。=\n所以iOS 7以后，UITableView有了一个新的属性：estimatedRowHeight。\n从属性名上就可以看出，这个属性可以为Cell预先指定一个“估计”的高度值，这样的话，系统就可以先按照估计值布局，然后只获取显示范围内的Cell的高度，这样就不会一次性计算全部的了。\n关键代码 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { static Case4Cell *templateCell; static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, ^{ templateCell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([Case4Cell class])]; }); // 获取对应的数据 Case4DataEntity *dataEntity = _data[(NSUInteger) indexPath.row]; // 填充数据 [templateCell setupData:dataEntity]; // 判断高度是否已经计算过 if (dataEntity.cellHeight \u0026lt;= 0) { // 根据当前数据，计算Cell的高度，注意+1 dataEntity.cellHeight = [templateCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1; } return dataEntity.cellHeight; } iOS 8的新特性 iOS 8大大简化了Cell的高度计算，只要设置好Cell的约束，添加下面几行代码：\ntableView.rowHeight = UITableViewAutomaticDimension; tableView.estimatedRowHeight = 80; 然后：\n- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // 只用返回这个！ return UITableViewAutomaticDimension; } 对！就只用这么几行代码就行！\nCase 2: topLayoutGuide和bottomLayoutGuide 是什么 topLayoutGuide和bottomLayoutGuide都是iOS 7以后，UIViewController的属性。\n在文档、头文件中，topLayoutGuide和bottomLayoutGuide的定义如下：\n@property(nonatomic,readonly,retain) id\u0026lt;UILayoutSupport\u0026gt; topLayoutGuide NS_AVAILABLE_IOS(7_0); @property(nonatomic,readonly,retain) id\u0026lt;UILayoutSupport\u0026gt; bottomLayoutGuide NS_AVAILABLE_IOS(7_0); 而UILayoutSupport的定义更是简单：\n@protocol UILayoutSupport \u0026lt;NSObject\u0026gt; @property(nonatomic,readonly) CGFloat length; @end 以topLayoutGuide为例：\nThe topLayoutGuide property comes into play when a view controller is frontmost onscreen. It indicates the highest vertical extent for content that you don’t want to appear behind a translucent or transparent UIKit bar (such as a status or navigation bar). This property implements the UILayoutSupport protocol and you can employ it as a constraint item when using the NSLayoutConstraint class.\n简单来说，topLayoutGuide表示当前页面的上方被status bar、navigation bar遮挡的部分。同理，bottomLayoutGuide表示下方被遮挡的部分。\n如下图:\n解决的问题 有些时候，一个ViewController可能单独显示出来，也可能内嵌在UINavigationController里面显示出来。在这两种情况下，页面的“可视范围”是不一样的，很明显，NavigationBar会遮挡住一部分，用了UITabBarController时，tabBar也会遮挡住下方一部分。再加上各种Bar都可以隐藏，情况会变得更复杂。\n难道要为每种情况去写一份布局代码？\n如何使用 为了解决上面的问题，就需要在设置约束时，加进topLayoutGuide和bottomLayoutGuide。\n用法1: 强制转换为UIView 定义上，topLayoutGuide和bottomLayoutGuide都是id，但是实际中是什么呢？跟UIView有什么关系？ 看看如下代码的运行结果：\nNSLog(@\u0026#34;%d\u0026#34;, [self.topLayoutGuide isKindOfClass:[UIView class]]); 结果是：”1”\n也就是说，在运行期间，topLayoutGuide和bottomLayoutGuide就是UIView的子类。\n所以，第一种方法就是强制转换成UIView，直接运用在Masonry的约束里面，正如较旧的Masonry官方示例中的一样：\n[topView makeConstraints:^(MASConstraintMaker *make) { // 强制转换 UIView *topLayoutGuide = (id)self.topLayoutGuide; make.top.equalTo(topLayoutGuide.mas_bottom); // ... }]; 但是这样存在着风险，万一哪天苹果改变了topLayoutGuide和bottomLayoutGuide的实现方法，这么用就Crash了=。=\n用法2: 直接使用length属性 第二种方法，就是直接使用UILayoutSupport定义的length属性。 这个时候就有个地方要特别注意，在运行到viewDidLoad的时候，length的值是0，因为这个时候界面还没有被绘制，所以一个解决方法就是在ViewController的updateViewConstraints方法里面去使用length值添加约束。如下：\n- (void)updateViewConstraints { [_topView mas_updateConstraints:^(MASConstraintMaker *make) { // 直接利用其length属性 make.top.equalTo(self.view.mas_top).with.offset(self.topLayoutGuide.length); }]; [super updateViewConstraints]; } 用法3: 使用新版的mas_topLayoutGuide和mas_bottomLayoutGuide 在Masonry的新版中，为UIViewController增加了一个新的Category: MASAdditions，增加了mas_topLayoutGuide和mas_bottomLayoutGuide两个方法，这样的话，我们就可以优雅的使用topLayoutGuide和bottomLayoutGuide了~\n[_topView mas_makeConstraints:^(MASConstraintMaker *make) { // 不用强制转换，也不用在updateViewConstraints里面更新了 make.top.equalTo(self.mas_topLayoutGuide); // ... }]; 示例 直接看Demo吧，比较简单。\nCase 3: 自定义baseline 最后一个Case，讲讲baseline。\nbaseline，翻译过来就是“基线”，在Autolayout里面对应着**NSLayoutFormatAlignAllBaseline**，也是一种对齐的标准。例如，UIButton的baseline就是内部的文字，如果一排button按照baseline对齐的话，就是下面这样：\n对于自定义的View来说，baseline默认就是整个view的底部，如果想改变baseline的话，可以重写UIView的viewForBaselineLayout，返回当成baseline的view即可。\n如下面的自定义view：\n很明显，baseline就是显示图片的UIImageView，代码也很简单:\n// 自定义的View中 // 返回自定义的baseline的view - (UIView *)viewForBaselineLayout { return _imageView; } 灵活的使用baseline，可以更加方便的进行布局。\n总结 写了好长，能全部看完的朋友，嗯，你是个优秀的程序员=。=\n后面打算用Swift的SnapKit把所有的Case全部实现一次。\n参考 细数AutoLayout以来UIView和UIViewController新增的相关API – UIView篇 Preventing the Status Bar from Covering Your Views Advanced Auto Layout Toolbox 细数AutoLayout以来UIView和UIViewController新增的相关API – UIViewController篇 AutoLayout实战:cell高度不固定的UITableView AutoLayout下多行UILabel无法显示多行文本的问题 AutoLayout深入浅出五-UITableView动态高度 Faster UITableViews with iOS 7 ","date":"2015-08-08T00:00:00Z","image":"/assets/2015/08/08/autolayout-example-with-masonry2/blog_autolayout_example_with_masonry2_1.gif","permalink":"/2015/08/08/autolayout-example-with-masonry2/","title":"有趣的Autolayout示例2-Masonry实现"},{"content":"更新 2015-11-16 感谢微博好友**@zyyy_000的评论，补充了为什么要在+ (void)load方法里面做Method Swizzling**。\n前言 最近，在做项目时，因为某种原因，突然要“适配”iOS6（也是醉了。。。），保证极少数的iOS6用户可以“用上”新的版本。哪怕界面上有瑕疵，只要功能正常就行。于是就只好花几天时间对iOS6进行紧急适配（心中一万头驼羊奔跑而过。。。）\n本文总结了一些常规的，和“非常规”的iOS项目向老版本兼容的办法，结合了宏定义、Category和Runtime，大家看着消遣一下就好哈~\n重点概念 首先强调一些概念。\nDeployment Target 和 Base SDK Deployment Target\n指的是你的APP能支持的最低系统版本，如要支持iOS6以上，就设置成iOS6即可。\nBase SDK\n指的是用来编译APP的SDK（Software Development Kit）的版本，一般保持当前XCode支持的最新的就好，如iOS8.4。SDK其实就是包含了所有的你要用到的头文件、链接库的集合，你的APP里面用的各种类、函数，能编译、链接成最后的安装包，就要靠它，苹果每次升级系统，新推出的各种API，也是在SDK里面。所以一般Base SDK肯定是大于等于Deployment Target的版本。\n区分\n既然Base SDK的版本大于等于Deployment Target的版本，那么就要小心了，因为**“只要用到的类、方法，在当前的Base SDK版本里面存在，就可以编译通过！但是一旦运行APP的手机的系统版本低于这些类、方法的最低版本要求，APP就会Crash！”**\n所以并不是说，能编译通过的，就一定能运行成功！还要在运行时检查！简单来说，就是如下图：\n宏只在编译时生效！ 宏定义只是纯粹的文本替换，只在编译时起作用。如下代码：\n#if __IPHONE_OS_VERSION_MIN_REQUIRED \u0026gt;= 70000 NSLog(@\u0026#34;Tutuge\u0026#34;); #endif 被宏定义包起来的代码是否会执行，在编译时就决定好了，无论你是用什么系统运行，宏定义再也没有什么卵用=。=\n编译时检查SDK版本，运行时检查系统版本 这个是最基本的适配手段。\n用到的宏如下：\n__IPHONE_OS_VERSION_MAX_ALLOWED: 值等于Base SDK，即用于检查SDK版本的。 __IPHONE_OS_VERSION_MIN_REQUIRED: 值等于Deployment Target，检查支持的最小系统版本。 运行时检查系统版本：\nif ([UIDevice currentDevice].systemVersion.floatValue \u0026gt; 8.0f) { // ... } 假如我们现在想用iOS8新的UIAlertController来显示提示框，应该如下判断：\n// 编译时判断：检查SDK版本 #if __IPHONE_OS_VERSION_MAX_ALLOWED \u0026gt; 80000 // 运行时判断：检查当前系统版本 if ([UIDevice currentDevice].systemVersion.floatValue \u0026gt; 8.0f) { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@\u0026#34;Tutuge\u0026#34; message:@\u0026#34;Compatibility\u0026#34; preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@\u0026#34;Cancel\u0026#34; style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { NSLog(@\u0026#34;Cancel\u0026#34;); }]]; [self presentViewController:alertController animated:YES completion:nil]; } else { // 用旧的代替 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@\u0026#34;Tutuge\u0026#34; message:@\u0026#34;Compatibility\u0026#34; delegate:nil cancelButtonTitle:@\u0026#34;Cancel\u0026#34; otherButtonTitles:nil]; [alertView show]; } #else // ... #endif 总的来说就是编译时、运行时的判断均不能少。\nWeakly Linked - 运行时检查类、方法是否可用 除了用宏、系统版本检测，还可以用Weakly Linked特性做运行时的检查。\n对于iOS4.2以上的，有NS_CLASS_AVAILABLE标示的类，可以如下判断是否可用：\n#if __IPHONE_OS_VERSION_MAX_ALLOWED \u0026gt; 80000 // Weakly Linked判断 if ([UIAlertController class]) { // 使用UIAlertController... } else { // 使用旧的方案... } #endif 也可以如下判断：\nClass class = NSClassFromString (@\u0026#34;UIAlertController\u0026#34;); if (class) { // 使用UIAlertController... } else { // 使用旧的方案... } 对于方法，如下判断:\nif ([UITableViewCell instancesRespondToSelector:@selector (setSeparatorInset:)]) { // ... } else { // ... } 至于用哪种方法，统一一下即可。\n用Method Swizzling做兼容 有关Runtime、Method Swizzling的资料很多，各位自行阅读哈~\n在+ (void)load方法里面做替换 这里提一下为什么要在+ (void)load方法里面做Method Swizzling。\n在Objective-C中，运行时会自动调用每个类的两个方法。+ (void)load会在类、Category初始加载时调用，+ (void)initialize会在第一次调用类的类方法或实例方法之前被调用。\n但是需要注意的是，+ (void)initialize是可以被Category覆盖重写的，并且有多个Category都重写了+ (void)initialize方法时，只会运行其中一个，所以在+ (void)initialize里面做Method Swizzling显然是不行的。\n而+ (void)load方法只要实现了，就一定会调用。具体为什么大家可以自行阅读Runtime的源码，或者查阅相关文章。\n用dispatch_once保证只运行一次 因为Method Swizzling的影响是全局的，而且一旦多次调用，会出错，所以这个时候用dispatch_once就再合适不过了~\n实例 下面就是利用Method Swizzling做兼容的一个例子。 有时候，不同版本之间，同一个类、View控件的默认属性可能都会变化，如UILabel的背景色在iOS6上，默认是白色，而iOS6以后是透明的！如果在每个用到UILabel的地方，都手动设置一次背景色，代价太大。这个时候就需要Runtime的“黑魔法”上场。\n就以设置UILabel的默认背景色透明为例，就是在UILabel初始化时，如initWithFrame之前，先设置好透明背景色，简单的示例如下：\n// 创建Category @implementation UILabel (TTGCompatibility) + (void)load { static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, ^{ // 先判断系统版本，尽量减少Runtime的作用范围 if ([UIDevice currentDevice].systemVersion.floatValue \u0026lt; 7.0f) { // Method Swizzling // initWithFrame Method oriMethod = class_getInstanceMethod(self, @selector(initWithFrame:)); Method newMethod = class_getInstanceMethod(self, @selector(compatible_initWithFrame:)); method_exchangeImplementations(oriMethod, newMethod); // initWithCoder... } }); } // initWithFrame - (id)compatible_initWithFrame:(CGRect)frame { id newSelf = [self compatible_initWithFrame:frame]; // 设置透明背景色 ((UILabel *)newSelf).backgroundColor = [UIColor clearColor]; return newSelf; } // initWithCoder... 运行时添加“Dummy”方法，减少代码改动 Dummy，意思是“假的、假动作、假人”，在这里指的是为旧版本不存在的方法提供一个“假的”替代方法，防止因新API找不到而导致的Crash。\n以UITableViewCell的**“setSeparatorInset:”**方法为例，在iOS6中，压根就不存在separatorInset，但是现有的代码里面大量的调用了这个方法，怎么办？难道一个一个的去加上判断条件？代价太大。\n这个时候就可以用Runtime的手段，在运行时添加一个Dummy方法，去“代替接收”setSeparatorInset消息，防止在iOS6上的Crash。\n代码如下：\n@implementation UITableViewCell (TTGCompatibility) + (void)load { // 编译时判断SDK #if __IPHONE_OS_VERSION_MAX_ALLOWED \u0026gt; __IPHONE_7_0 // 运行时判断系统版本 if ([UIDevice currentDevice].systemVersion.floatValue \u0026lt; 7.0f) { Method newMethod = class_getInstanceMethod(self, @selector(compatible_setSeparatorInset:)); // 增加Dummy方法 class_addMethod( self, @selector(setSeparatorInset:), method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); } #endif } // setSeparatorInset: 的Dummy方法 - (void)compatible_setSeparatorInset:(UIEdgeInsets) inset { // 空方法都可以，只是为了接收setSeparatorInset:消息。 } 总结 在适配旧版本时，除了基本的宏定义、[UIDevice currentDevice].systemVersion判断，适当的用Runtime，可以大大减少对现有代码的“干涉”，多种方法相结合才是最好的。\n嗯，还在用iOS6的用户，升个级呗=。=\n参考 SDK Compatibility Guide iOS开发之多系统版本兼容 Weak Linking - check if a class exists and use that class Xcode preprocessor macro to check if Base SDK \u0026gt;= iOS 7.0 iOS SDK 兼容性指南 ","date":"2015-07-25T00:00:00Z","image":"/assets/2015/07/25/compatibility-with-macro-category-and-runtime/blog_compatibility-with-macro-category-and-runtime_1.png","permalink":"/2015/07/25/compatibility-with-macro-category-and-runtime/","title":"解决iOS项目的版本兼容问题-结合宏、Category和Runtime"},{"content":"前言 好久没有写东西了，忙啊。\n前段时间参加了一下我们华科联创的HackDay（本人在读研=。=，目前在阿里实习），作品是一款实时在线对战游戏 - 波波攒，（介绍请看知乎） 从iOS游戏客户端（用的SpriteKit）到后台（PHP CI + Node + SocketIO + MySQL）全是自己一个人倒腾出来的，做了一把真正的全栈工程师，爽啊~ 后面会完善整个游戏，增加角色、优化啥的，过上一段时间会上线的哈~\n回到正文，本文主要介绍了怎么用Runtime的手段遍历任意NSObject对象的所有property，检查其值是否是nil，是的话根据其类型为其填充一个默认值。 Runtime毕竟是个“危险”的技术，本文的代码只是个初步的尝试。\n初衷 在做项目的过程中，总是会写一大堆if、else语句去检查对象的Property是否是nil，如从服务器返回的JSON中缺少属性，导致Entity的某些值为空；或者创建的对象没有对所有属性做初始化等等。写多了觉得好烦啊=。= 所以想到本文的方法，嗯，程序员总是懒的。\n解决步骤 遍历一个对象的所有属性（默认不包括父类属性）。 判断属性是否是nil。 为nil的属性，获取它的类型。 根据类型设置初始值（如NSString可以设为空字符串；NSNumber可以设为@0） Runtime OC是一门“动态”、“基于消息”的语言，而Runtime就是利用OC的动态特性，在运行时对程序做出“调整”的技术。有关Runtime的官方文档、网上的资料很多，大家自学哈~\n本文主要用了如下几个Runtime的函数：\n// 获取类的所有Property 1. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) // 获取一个Property的变量名 2. const char *property_getName(objc_property_t property) // 获取一个Property的详细类型表达字符串 3. const char *property_getAttributes(objc_property_t property) 示例 不好一块一块拆开说，直接上代码：\n/** * 解析Property的Attributed字符串，参考Stackoverflow */ static const char *getPropertyType(objc_property_t property) { const char *attributes = property_getAttributes(property); NSLog(@\u0026#34;%s\u0026#34;, attributes); char buffer[1 + strlen(attributes)]; strcpy(buffer, attributes); char *state = buffer, *attribute; while ((attribute = strsep(\u0026amp;state, \u0026#34;,\u0026#34;)) != NULL) { // 非对象类型 if (attribute[0] == \u0026#39;T\u0026#39; \u0026amp;\u0026amp; attribute[1] != \u0026#39;@\u0026#39;) { // 利用NSData复制一份字符串 return (const char *) [[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] bytes]; // 纯id类型 } else if (attribute[0] == \u0026#39;T\u0026#39; \u0026amp;\u0026amp; attribute[1] == \u0026#39;@\u0026#39; \u0026amp;\u0026amp; strlen(attribute) == 2) { return \u0026#34;id\u0026#34;; // 对象类型 } else if (attribute[0] == \u0026#39;T\u0026#39; \u0026amp;\u0026amp; attribute[1] == \u0026#39;@\u0026#39;) { return (const char *) [[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes]; } } return \u0026#34;\u0026#34;; } /** * 给对象的属性设置默认值 */ void checkEntity(NSObject *object) { // 不同类型的字符串表示，目前只是简单检查字符串、数字、数组 static const char *CLASS_NAME_NSSTRING; static const char *CLASS_NAME_NSNUMBER; static const char *CLASS_NAME_NSARRAY; // 初始化类型常量 static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, ^{ // \u0026#34;NSString\u0026#34; CLASS_NAME_NSSTRING = NSStringFromClass([NSString class]).UTF8String; // \u0026#34;NSNumber CLASS_NAME_NSNUMBER = NSStringFromClass([NSNumber class]).UTF8String; // \u0026#34;NSArray\u0026#34; CLASS_NAME_NSARRAY = NSStringFromClass([NSArray class]).UTF8String; }); @try { unsigned int outCount, i; // 包含所有Property的数组 objc_property_t *properties = class_copyPropertyList([object class], \u0026amp;outCount); // 遍历每个Property for (i = 0; i \u0026lt; outCount; i++) { // 取出对应Property objc_property_t property = properties[i]; // 获取Property对应的变量名 NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)]; // 获取Property的类型名 const char *propertyTypeName = getPropertyType(property); // 获取Property的值 id propertyValue = [object valueForKey:propertyName]; // 值为空，才设置默认值 if (!propertyValue) { // NSString if (strncmp(CLASS_NAME_NSSTRING, propertyTypeName, strlen(CLASS_NAME_NSSTRING)) == 0) { [object setValue:@\u0026#34;\u0026#34; forKey:propertyName]; } // NSNumber if (strncmp(CLASS_NAME_NSNUMBER, propertyTypeName, strlen(CLASS_NAME_NSNUMBER)) == 0) { [object setValue:@0 forKey:propertyName]; } // NSArray if (strncmp(CLASS_NAME_NSARRAY, propertyTypeName, strlen(CLASS_NAME_NSARRAY)) == 0) { [object setValue:@[] forKey:propertyName]; } } } // 别忘了释放数组 free(properties); } @catch (NSException *exception) { NSLog(@\u0026#34;Check Entity Exception: %@\u0026#34;, [exception description]); } } 重点 - 解析property_getAttributes函数的结果 在整个处理过程中，property_getAttributes函数是关键，因为我们要首先确定Property的类型，才能根据类型赋初值，但是property_getAttributes函数返回的字符串比较“晦涩难懂”：\n如下定义的Property：\n@property (copy, nonatomic) NSString *name; @property (strong, nonatomic) NSNumber *number; @property (strong, nonatomic) NSArray *array; @property (assign, nonatomic) NSInteger i; @property (assign, nonatomic) CGFloat f; @property (assign, nonatomic) char *cStr; 依次通过property_getAttributes获取的结果是：\nT@\u0026#34;NSString\u0026#34;,C,N,V_name T@\u0026#34;NSNumber\u0026#34;,\u0026amp;,N,V_number T@\u0026#34;NSArray\u0026#34;,\u0026amp;,N,V_array Tq,N,V_i Td,N,V_f T*,N,V_cStr 参考Declared Properties of Objective-C Runtime Programming Guide 我们大概可以知道，T表示Type，后面跟着@表示Cocoa对象类型，后面的表示Property的属性，如Copy、strong等，然后就是变量名。 所以getPropertyType函数的工作就是纯粹的解析字符串，获取T@后面的类型名。\n效果 例如我们有如下对象：\n@interface UserEntity : NSObject @property (copy, nonatomic) NSString *name; @property (strong, nonatomic) NSNumber *number; @property (strong, nonatomic) NSArray *array; @end 设置默认值：\nUserEntity *userEntity = [UserEntity new]; // 检查属性，设置默认值。 checkEntity(userEntity); // 使用... NSLog(@\u0026#34;name: %@\u0026#34;, userEntity.name); NSLog(@\u0026#34;number: %@\u0026#34;, userEntity.number); NSLog(@\u0026#34;array: %@\u0026#34;, userEntity.array); 输出：\n2015-07-11 18:17:25.918 Common[6939:270543] name: 2015-07-11 18:17:25.918 Common[6939:270543] number: 0 2015-07-11 18:17:25.918 Common[6939:270543] array: ( ) 这样，一个对象的所有Property都有了初值。\n总结 上面的例子只是个粗略的版本，只是检查了字符串、数字、数组，其实完全可以扩展出很多功能，如针对不同的类型，根据对象的类型，设置不同的默认初值等，靠读者你了~\nRuntime是个好东西，但是别乱用啊=。=\n参考 Objective-C Runtime Reference Objective-C Runtime Programming Guide - Declared Properties Get an object properties list in Objective-C ","date":"2015-07-08T00:00:00Z","permalink":"/2015/07/08/fill-nil-property-of-object/","title":"用Runtime的手段填充任意NSObject对象的nil属性"},{"content":"更新 2016-08-06: 第四篇: 有趣的Autolayout示例4-Masonry实现 2015-12-23: 感谢XVXVXXX的PR，更新了Case2 2015-12-15: 第三篇：有趣的Autolayout示例3-Masonry实现 2015-11-27: 感谢“谢小雷”指出打字错误~ 2015-9-10: 第二篇: 有趣的Autolayout示例2-Masonry实现 2015-9-1: 感谢“王振宇C艹”指出打字错误，已经修改了哈~ 前言 好久没有写Blog了，这段时间有点忙啊=。= 本文举了3个比较有“特点”的Autolayout例子，源于微博上好友的提问，感觉比较有意思，也比较有代表性，就写了出来，分享给大家~ 至于为什么用Masonry，那是因为它好用啊！（被问到过有关Masonry的问题，就索性用它来实现吧=。=）。\n效果图 Github地址 https://github.com/zekunyan/AutolayoutExampleWithMasonry\n关于例子工程结构 实现的时候采用的是用StoryBoard拖拽约束+Masonry手写代码相结合的方式实现。最关键的地方是用Masonry，为了更好地突出重点。其它的无关紧要的空间约束，直接就拖拽了。\n关于Autolayout 刚开始学习Autolayout的时候，什么“Leading Edges”、“Horizontal Centers”，好多啊，感觉一下子适应不来，有时候面对一个界面布局上的需求，可能都无从下手。\n总的来说，我觉得Autolayout的关键就是“Constraint（约束）”。其实就是以下两点：\n从显式设置frame的属性，到利用约束控制View的大小、位置。 思考如何布局时，重点从单个的View，到整体所有View之间的相互关系。 既然没有了具体设置View的frame属性，也就是说，系统会在运行时，通过我们设定的“约束”，计算出每个View的frame，再去绘制屏幕内容。\n也就是说，我们设置的Constraint，要能体现出View的位置（x、y坐标）、大小（宽高）。无论是用IB拖拽约束，还是手写代码，只要从这个角度去思考，很多问题就都能解决。\n有关Autolayout的知识，网上有很多，在这里就不详细列出了，但是有个公式倒是可以贴出来：\nviewA-attribute = viewB-attribute * multiplier + constant 关于Masonry 好用！\nCase 1: 并排两个label，宽度由内容决定。父级View宽度不够时，优先显示左边label的内容 遇到这种跟内容压缩、优先级有关的布局，就不得不提Autolayout中的两个重要的属性“Content Compression Resistance”和“Content Hugging”。\nContent Compression Resistance = 不许挤我！ 对，这个属性说白了就是“不许挤我”=。= 这个属性的优先级（Priority）越高，越不“容易”被压缩。也就是说，当整体的空间装不下所有的View的时候，Content Compression Resistance优先级越高的，显示的内容越完整。\nContent Hugging = 抱紧！ 这个属性的优先级越高，整个View就要越“抱紧”View里面的内容。也就是View的大小不会随着父级View的扩大而扩大。\n分析 根据要求，可以将约束分为两个部分：\n整体空间足够时，两个label的宽度由内容决定，也就是说，label的“Content Hugging”优先级很高，而且没有固定的Width属性。 整体空间不够时，左边的label更不容易被压缩，也就是“Content Compression Resistance”优先级更高。 重点：\nlabel不设置具体的宽度（width）属性，宽度由内容决定。 显示的优先级由“Content Compression Resistance”属性的高低决定。 约束示例图 关键代码 关键的代码如下：（label1是左边的label，label2是右边的）\n设置位置 // label1: 位于左上角 [_label1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(_contentView1.mas_top).with.offset(5); make.left.equalTo(_contentView1.mas_left).with.offset(2); // 40高度 make.height.equalTo(@40); }]; // label2: 位于右上角 [_label2 mas_makeConstraints:^(MASConstraintMaker *make) { //左边贴着label1，间隔2 make.left.equalTo(_label1.mas_right).with.offset(2); //上边贴着父view，间隔5 make.top.equalTo(_contentView1.mas_top).with.offset(5); //右边的间隔保持大于等于2，注意是lessThanOrEqual //这里的“lessThanOrEqualTo”放在从左往右的X轴上考虑会更好理解。 //即：label2的右边界的X坐标值“小于等于”containView的右边界的X坐标值。 make.right.lessThanOrEqualTo(_contentView1.mas_right).with.offset(-2); //只设置高度40 make.height.equalTo(@40); }]; 设置内容约束 //设置label1的content hugging 为1000 [_label1 setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; //设置label1的content compression 为1000 [_label1 setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; //设置右边的label2的content hugging 为1000 [_label2 setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; //设置右边的label2的content compression 为250 [_label2 setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; 小节 灵活运用“Content Compression Resistance”和“Content Hugging”属性。\nCase 2: 四个ImageView整体居中，可以任意显示、隐藏 先看看示例的截图：\n下面的四个Switch控件分别控制上面对应位置的图片是否显示。\n分析 首先就是整体居中，为了实现这个，最简单的办法就是将四个图片“装进”一个容器View里面，然后让这个容器View在整个页面中居中即可。这样就不用控制每个图片的居中效果了。 然后就是显示与隐藏。在这里我直接控制图片ImageView的宽度，宽度为0的时候不就“隐藏”了吗。 约束示例图 解释\n之所以这么设置，主要目的有以下几点：\n尽量减少无效的约束，保证约束不多也不少。 内部的每个imageView约束其实都只有四个：left、centerY、width和height，这样有个好处，就是可以在循环里面依次添加约束，大大减少代码量。 最右边的imageView还要单独设置跟容器View的右边约束，是为了不用设置容器View的width，保证容器View是刚好包含内部的View的，这样整体才是居中的。 关键代码 先看看最外层容器View的代码：\n//containerView 就是 容器View [_containerView mas_makeConstraints:^(MASConstraintMaker *make) { //只设置高度，宽度由子View决定 make.height.equalTo(@(IMAGE_SIZE)); //水平居中 make.centerX.equalTo(self.view.mas_centerX); //距离父View顶部200点 make.top.equalTo(self.view.mas_top).offset(200); }]; 循环创建每个内部的imageView\n//循环创建、添加imageView for (NSUInteger i = 0; i \u0026lt; 4; i++) { UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:_imageNames[i]]]; [_imageViews addObject:imageView]; [_containerView addSubview:imageView]; } 最后是循环对imageView加上约束： 感谢XVXVXXX的PR:-D\n//设定大小 CGSize imageViewSize = CGSizeMake(IMAGE_SIZE, IMAGE_SIZE); //分别设置每个imageView的宽高、左边、垂直中心约束，注意约束的对象 //每个View的左边约束和左边的View的右边相等=。=，有点绕口... // 保存循环中的临时结果 __block UIView *lastView = nil; __block MASConstraint *widthConstraint = nil; NSUInteger arrayCount = _imageViews.count; [_imageViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) { [view mas_makeConstraints:^(MASConstraintMaker *make) { //宽高固定 widthConstraint = make.width.equalTo(@(imageViewSize.width)); make.height.equalTo(@(imageViewSize.height)); //左边约束 make.left.equalTo(lastView ? lastView.mas_right : view.superview.mas_left); //垂直中心对齐 make.centerY.equalTo(view.superview.mas_centerY); //设置最右边的imageView的右边与父view的最右对齐 if (idx == arrayCount - 1) { make.right.equalTo(view.superview.mas_right); } [_widthConstraints addObject:widthConstraint]; lastView = view; }]; }]; 控制ImageView显示、隐藏的时候，直接让其宽度等于0就行：\n- (IBAction)showOrHideImage:(UISwitch *)sender { NSUInteger index = (NSUInteger) sender.tag; MASConstraint *width = _widthConstraints[index]; if (sender.on) { width.equalTo(@(IMAGE_SIZE)); } else { width.equalTo(@0); } } 小节 有时候用个“容器View”管理内部的View，往往会起到事半功倍的效果。而且在组织约束的时候，尽量的将约束统一起来，这样可以用一个函数去设置，减少代码量。\nCase 3: 子View的宽度始终是父级View的一半（或者任意百分比） 其实这个很简单=。= 再看看这个公式：\nviewA-attribute = viewB-attribute * multiplier + constant 这个是Autolayout里面一个约束的不同属性的基本组合关系，替换成宽度的话，就是下面这样：\n子View的宽度 = 父级View宽度 * 系数 + 常数； 在Masonry里面，其实有个函数“multipliedBy”，就是用来设置multipler属性的（跟原本的NSLayoutConstraint的对应）。\n关键代码 如下：\n[subView mas_makeConstraints:^(MASConstraintMaker *make) { //上下左贴边 make.left.equalTo(_containerView.mas_left); make.top.equalTo(_containerView.mas_top); make.bottom.equalTo(_containerView.mas_bottom); //宽度为父view的宽度的一半 make.width.equalTo(_containerView.mas_width).multipliedBy(0.5); }]; 接着，只要控制父级View的宽度，子View的宽度就会随着变化了。\n小节 multipliedBy在Masonry的Github主页里面没有=。= 所以要养成读头文件的习惯~\n总结 有关Autolayout的东西还有好多没有写，什么动画啊、动态修改约束之类的，本文也算是个引子吧，任重而道远~\n能看到这的朋友，也算是很有耐心了，哈哈~~\n参考 SnapKit/Masonry Masonry介绍与使用实践(快速上手Autolayout) AutoLayout:忘掉Frame,拥抱Constraint Autolayout 基础 对Auto Layout中的Content Compression Resistance和Content Hugging的总结 Auto Layout Guide ","date":"2015-05-23T00:00:00Z","image":"/assets/2015/05/23/autolayout-example-with-masonry/blog_autolayout_example_with_masonry_1.gif","permalink":"/2015/05/23/autolayout-example-with-masonry/","title":"有趣的Autolayout示例-Masonry实现"},{"content":"更新 2015-11-27：增加了利用NSTextAttachment的bounds属性修改大小的方法。 前言 本文是基于UITextView编辑时插入自定义表情-简单的图文混编写的，主要实现了在UITextView插入表情图片时，自定义表情大小的功能。\nGithub 本文代码工程地址：https://github.com/zekunyan/UITextViewDIYEmojiExample\nGif示例图 关键点 首先要明确我们要什么，很简单，就是指定NSTextAttachment在被绘制时的大小。\n所以，按照这个思路，就可以去找找NSTextAttachment类的相关方法，看能不能通过继承或其他的方式改变大小。\n方法1 - NSTextAttachmentContainer NSTextAttachment实现了“NSTextAttachmentContainer”这个Protocol，而这个Protocol里面有如下方法：\n- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex 再看看其解释：\nReturns the layout bounds of the text attachment to the layout manager. (required) 也就是说，在绘制NSTextAttachment的内容的时候，内容的“Bounds”是通过这个方法获得的。所以，可以重写这个方法，来达到我们的目的。\n方法2 - 直接修改bounds NSTextAttachment还有一个属性@property(nonatomic) CGRect bounds，其解释如下：\nDefines the layout bounds of the receiver\u0026#39;s graphical representation in the text coordinate system. 所以，修改这个也可以达到目的。\n实现方法1 - 继续扩展NSTextAttachment类 保存自定义Size 根据之前的定义，我们在自定义的类“EmojiTextAttachment”中再加一个保存大小的属性，如下：\n//EmojiTextAttachment类定义 @interface EmojiTextAttachment : NSTextAttachment //表情的字符串表示，见前文 @property(strong, nonatomic) NSString *emojiTag; //新增：保存当前表情图片的大小 @property(assign, nonatomic) CGSize emojiSize; @end 有了“emojiSize”这个属性，我们就可以在自由的指定每个NSTextAttachment的大小。\n重写 接下来就是重写方法，不多说，见代码：\n//EmojiTextAttachment实现 @implementation EmojiTextAttachment //重点！重写NSTextAttachmentContainer Protocol的方法 - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex { // 返回我们指定的size return CGRectMake(0, 0, _emojiSize.width, _emojiSize.height); } @end 实现方法2 直接在创建表情NSTextAttachment的时候设置bounds属性即可。\n使用 增加了emojiSize属性后，我们就可以在创建表情，甚至创建后，自由的调整每个NSTextAttachment的大小。\n总结 一共只增加了十几行代码，但是效果还是不错哒~\n参考 NSTextAttachment NSTextAttachmentContainer ","date":"2015-04-21T00:00:00Z","image":"/assets/2015/04/21/resizable-nstextattachment/blog_resizable_nstextattachment_1.gif","permalink":"/2015/04/21/resizable-nstextattachment/","title":"UITextView编辑时插入自定义表情-续-自定义表情图片的大小"},{"content":"更新 2016-02-22: Response对象不用主动创建。 前言 前段时间用了一下Facebook的开源RPC框架Thrift，做PHP客户端调用C++后端程序，真心觉得Thrift不错！\n本文项目地址：https://github.com/zekunyan/ThriftDemo_PHP_CPP\n先看看本文的例子示意图：\n流程\nPHP客户端发起请求，请求参数是“Request”类型，里面有studentID参数。 CPP服务端收到请求返回数据，返回类型为“Response”，里面包含了student的信息，此处只是简单的示例。 定义 什么是RPC 远程过程调用（英语：Remote Procedure Call，缩写为 RPC）是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序，而程序员无需额外地为这个交互作用编程。 – 维基百科 - 远程过程调用\n通俗点讲，就是跨计算机、跨网络调用。\n什么是Thrift Apache Thrift 是Facebook实现的一种高效的、支持多种编程语言的远程服务调用(RPC)的框架。它采用接口描述语言定义并创建服务，支持可扩展的跨语言服务开发，所包含的代码生成引擎可以在多种语言中，如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等创建高效的、无缝的服务，其传输数据采用二进制格式，相对 XML 和 JSON 体积更小，对于高并发、大数据量和多语言的环境更有优势。 – Apache Thrift - 可伸缩的跨语言服务开发框架\n开发流程 配置环境。C++的如CMake、make、g++；PHP的如php、php-fpm、Apache（Nginx）。 根据需求，编写thrift接口定义文件（IDL定义文件）。 使用thrift程序，为不同的语言生成代码。 根据需求，修改生成的代码（主要是Server端），编写实际的业务逻辑。 编译、部署。 Thrift的接口定义文件 在利用Thrift开发的过程中，重点就是编写接口定义文件。 一般来说，接口定义文件决定了RPC过程中的通信数据结构、通信接口定义等。\n总的来说，thrift的接口定义语法类似于C语言，包含了struct、enum、map、list等基础数据结构，同时支持大部分基本数据类型，如32位整型“i32”等。\n详细的接口定义请参考：\nThrift interface description language example.thrift 看看本例子中的定义文件“TTG.thrift”：\nnamespace cpp TTG namespace php TTG enum ResponseState { StateOk = 0, StateError = 1, StateEmpty = 2 } struct Request { 1: i32 studentID = 0 } struct Response { 1: i32 studentID = 0, 2: string name, 3: list\u0026lt;string\u0026gt; infos, 4: ResponseState state } service TTGService { Response getStudentInfo(1: Request request); } 先定义命名空间，尽量减少命名冲突。 Request类型：PHP客户端请求的数据类型。 Response类型：CPP服务端返回的数据类型。 ResponseState枚举：定义返回的状态。 TTGService：服务接口定义。 生成对应语言的代码！ 是的，生成代码！这是我觉得Thrift框架最“神奇”的地方。我们只需要执行几条命令，就可以根据接口定义文件“生成”对应语言的代码。然后我们只需要将对应的业务逻辑加入到生成的代码中即可。\n如生成CPP服务端的代码：\nthrift --gen cpp TTG.thrift 然后就会生成如下代码。\nTTGService.cpp TTGService.h TTGService_server.skeleton.cpp TTG_constants.cpp TTG_constants.h TTG_types.cpp TTG_types.h 根据需要修改生成的代码 生成CPP、PHP端的代码以后，我们就可以根据需要修改、添加业务代码。\nCPP服务端 在生成CPP的代码时，会生成一个“TTGService_server.skeleton.cpp”文件，这个就是我们的CPP服务端的代码“架子”。其中重点如下：\n//实现调用的接口 class TTGServiceHandler : virtual public TTGServiceIf { public: TTGServiceHandler() { // Your initialization goes here } //接口实现部分，实现getStudentInfo这个接口的具体细节，如根据request创建、返回对应的response void getStudentInfo(Response\u0026amp; _return, const Request\u0026amp; request) { // Your implementation goes here printf(\u0026#34;getStudentInfo\\n\u0026#34;); } }; //... 我们将其改成：\nvoid getStudentInfo(Response \u0026amp;_return, const Request \u0026amp;request) { // 输出请求参数 cout\u0026lt;\u0026lt;\u0026#34;Request: \u0026#34;\u0026lt;\u0026lt;request.studentID\u0026lt;\u0026lt;endl; //填充数据，Response对象不用主动创建，进入函数时已经创建好了 _return.studentID = request.studentID; _return.name = \u0026#34;tutuge\u0026#34;; _return.infos.push_back(\u0026#34;Info 1\u0026#34;); _return.infos.push_back(\u0026#34;Info 2\u0026#34;); _return.state = ResponseState::StateOk; } 至此，CPP服务端的编写就完成了，接下来我们只需要编译、链接，执行最终生成的可执行文件即可。\nPHP客户端 PHP客户端的编写比较简单，直接参考代码吧：https://github.com/zekunyan/ThriftDemo_PHP_CPP/blob/master/PHP/client.php\n编译、链接，运行 用CMake，或者直接编写makefile均可，保证Thrift装好就行了。\n直接运行最后生成的可执行文件TTG.run即可。\n从浏览器，或者直接运行client.php，即可看到如下输出：\nobject(TTG\\Response)[9] public \u0026#39;studentID\u0026#39; =\u0026gt; int 100 public \u0026#39;name\u0026#39; =\u0026gt; string \u0026#39;tutuge\u0026#39; (length=6) public \u0026#39;infos\u0026#39; =\u0026gt; array (size=2) 0 =\u0026gt; string \u0026#39;Info 1\u0026#39; (length=6) 1 =\u0026gt; string \u0026#39;Info 2\u0026#39; (length=6) public \u0026#39;state\u0026#39; =\u0026gt; int 0 总结 Thrift是个好东西！就是文档好少=。=\n参考 Apache Thrift Thrift PHP Tutorial Thrift C++ Tutorial Apache Thrift Apache Thrift - 可伸缩的跨语言服务开发框架 ","date":"2015-04-19T00:00:00Z","image":"/assets/2015/04/19/thrift-example-cpp-and-php/blog_thrift_example_cpp_and_php_1.jpg","permalink":"/2015/04/19/thrift-example-cpp-and-php/","title":"RPC框架Thrift例子-PHP调用C++后端程序"},{"content":"前言 GCD(Grand Central Dispatch)可以说是Mac、iOS开发中的一大“利器”，本文就总结一些有关使用GCD的经验与技巧。\ndispatch_once_t必须是全局或static变量 这一条算是“老生常谈”了，但我认为还是有必要强调一次，毕竟非全局或非static的dispatch_once_t变量在使用时会导致非常不好排查的bug，正确的如下：\n//静态变量，保证只有一份实例，才能确保只执行一次 static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, ^{ //单例代码 }); 其实就是保证dispatch_once_t只有一份实例。\ndispatch_queue_create的第二个参数 dispatch_queue_create，创建队列用的，它的参数只有两个，原型如下：\ndispatch_queue_t dispatch_queue_create ( const char *label, dispatch_queue_attr_t attr ); 在网上的大部分教程里（甚至Apple自己的文档里），都是这么创建串行队列的：\ndispatch_queue_t queue = dispatch_queue_create(\u0026#34;com.example.MyQueue\u0026#34;, NULL); 看，第二个参数传的是“NULL”。 但是dispatch_queue_attr_t类型是有已经定义好的常量的，所以我认为，为了更加的清晰、严谨，最好如下创建队列：\n//串行队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;com.example.MyQueue\u0026#34;, DISPATCH_QUEUE_SERIAL); //并行队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;com.example.MyQueue\u0026#34;, DISPATCH_QUEUE_CONCURRENT); 常量就是为了使代码更加“易懂”，更加清晰，既然有，为啥不用呢~\ndispatch_after是延迟提交，不是延迟运行 先看看官方文档的说明：\nEnqueue a block for execution at the specified time. Enqueue，就是入队，指的就是将一个Block在特定的延时以后，加入到指定的队列中，不是在特定的时间后立即运行！。\n看看如下代码示例：\n//创建串行队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); //立即打印一条信息 NSLog(@\u0026#34;Begin add block...\u0026#34;); //提交一个block dispatch_async(queue, ^{ //Sleep 10秒 [NSThread sleepForTimeInterval:10]; NSLog(@\u0026#34;First block done...\u0026#34;); }); //5 秒以后提交block dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), queue, ^{ NSLog(@\u0026#34;After...\u0026#34;); }); 结果如下：\n2015-03-31 20:57:27.122 GCDTest[45633:1812016] Begin add block... 2015-03-31 20:57:37.127 GCDTest[45633:1812041] First block done... 2015-03-31 20:57:37.127 GCDTest[45633:1812041] After... 从结果也验证了，dispatch_after只是延时提交block，并不是延时后立即执行。所以想用dispatch_after精确控制运行状态的朋友可要注意了~\n正确创建dispatch_time_t 用dispatch_after的时候就会用到dispatch_time_t变量，但是如何创建合适的时间呢？答案就是用dispatch_time函数，其原型如下：\ndispatch_time_t dispatch_time ( dispatch_time_t when, int64_t delta ); 第一个参数一般是DISPATCH_TIME_NOW，表示从现在开始。 那么第二个参数就是真正的延时的具体时间。\n这里要特别注意的是，delta参数是“纳秒！”，就是说，延时1秒的话，delta应该是“1000000000”=。=，太长了，所以理所当然系统提供了常量，如下：\n#define NSEC_PER_SEC 1000000000ull #define USEC_PER_SEC 1000000ull #define NSEC_PER_USEC 1000ull 关键词解释：\nNSEC：纳秒。 USEC：微妙。 SEC：秒 PER：每 所以：\nNSEC_PER_SEC，每秒有多少纳秒。 USEC_PER_SEC，每秒有多少毫秒。（注意是指在纳秒的基础上） NSEC_PER_USEC，每毫秒有多少纳秒。 所以，延时1秒可以写成如下几种：\ndispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC); dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC); dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC); 最后一个“USEC_PER_SEC * NSEC_PER_USEC”，翻译过来就是“每秒的毫秒数乘以每毫秒的纳秒数”，也就是“每秒的纳秒数”，所以，延时500毫秒之类的，也就不难了吧~\ndispatch_suspend != 立即停止队列的运行 dispatch_suspend，dispatch_resume提供了“挂起、恢复”队列的功能，简单来说，就是可以暂停、恢复队列上的任务。但是这里的“挂起”，并不能保证可以立即停止队列上正在运行的block，看如下例子：\ndispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); //提交第一个block，延时5秒打印。 dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:5]; NSLog(@\u0026#34;After 5 seconds...\u0026#34;); }); //提交第二个block，也是延时5秒打印 dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:5]; NSLog(@\u0026#34;After 5 seconds again...\u0026#34;); }); //延时一秒 NSLog(@\u0026#34;sleep 1 second...\u0026#34;); [NSThread sleepForTimeInterval:1]; //挂起队列 NSLog(@\u0026#34;suspend...\u0026#34;); dispatch_suspend(queue); //延时10秒 NSLog(@\u0026#34;sleep 10 second...\u0026#34;); [NSThread sleepForTimeInterval:10]; //恢复队列 NSLog(@\u0026#34;resume...\u0026#34;); dispatch_resume(queue); 运行结果如下：\n2015-04-01 00:32:09.903 GCDTest[47201:1883834] sleep 1 second... 2015-04-01 00:32:10.910 GCDTest[47201:1883834] suspend... 2015-04-01 00:32:10.910 GCDTest[47201:1883834] sleep 10 second... 2015-04-01 00:32:14.908 GCDTest[47201:1883856] After 5 seconds... 2015-04-01 00:32:20.911 GCDTest[47201:1883834] resume... 2015-04-01 00:32:25.912 GCDTest[47201:1883856] After 5 seconds again... 可知，在dispatch_suspend挂起队列后，第一个block还是在运行，并且正常输出。 结合文档，我们可以得知，dispatch_suspend并不会立即暂停正在运行的block，而是在当前block执行完成后，暂停后续的block执行。\n所以下次想暂停正在队列上运行的block时，还是不要用dispatch_suspend了吧~\n“同步”的dispatch_apply dispatch_apply的作用是在一个队列（串行或并行）上“运行”多次block，其实就是简化了用循环去向队列依次添加block任务。但是我个人觉得这个函数就是个“坑”，先看看如下代码运行结果：\n//创建异步串行队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); //运行block3次 dispatch_apply(3, queue, ^(size_t i) { NSLog(@\u0026#34;apply loop: %zu\u0026#34;, i); }); //打印信息 NSLog(@\u0026#34;After apply\u0026#34;); 运行的结果是：\n2015-04-01 00:55:40.854 GCDTest[47402:1893289] apply loop: 0 2015-04-01 00:55:40.856 GCDTest[47402:1893289] apply loop: 1 2015-04-01 00:55:40.856 GCDTest[47402:1893289] apply loop: 2 2015-04-01 00:55:40.856 GCDTest[47402:1893289] After apply 看，明明是提交到异步的队列去运行，但是“After apply”居然在apply后打印，也就是说，dispatch_apply将外面的线程（main线程）**“阻塞”**了！\n查看官方文档，dispatch_apply确实会“等待”其所有的循环运行完毕才往下执行=。=，看来要小心使用了。\n避免死锁！ dispatch_sync导致的死锁 涉及到多线程的时候，不可避免的就会有“死锁”这个问题，在使用GCD时，往往一不小心，就可能造成死锁，看看下面的“死锁”例子：\n//在main线程使用“同步”方法提交Block，必定会死锁。 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@\u0026#34;I am block...\u0026#34;); }); 你可能会说，这么低级的错误，我怎么会犯，那么，看看下面的：\n- (void)updateUI1 { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@\u0026#34;Update ui 1\u0026#34;); //死锁！ [self updateUI2]; }); } - (void)updateUI2 { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@\u0026#34;Update ui 2\u0026#34;); }); } 在你不注意的时候，嵌套调用可能就会造成死锁！所以为了“世界和平”=。=，我们还是少用dispatch_sync吧。\ndispatch_apply导致的死锁！ 啥，dispatch_apply导致的死锁？。。。是的，前一节讲到，dispatch_apply会等循环执行完成，这不就差不多是阻塞了吗。看如下例子：\ndispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); dispatch_apply(3, queue, ^(size_t i) { NSLog(@\u0026#34;apply loop: %zu\u0026#34;, i); //再来一个dispatch_apply！死锁！ dispatch_apply(3, queue, ^(size_t j) { NSLog(@\u0026#34;apply loop inside %zu\u0026#34;, j); }); }); 这端代码只会输出“apply loop: 1”。。。就没有然后了=。=\n所以，一定要避免dispatch_apply的嵌套调用。\n灵活使用dispatch_group 很多时候我们需要等待一系列任务（block）执行完成，然后再做一些收尾的工作。如果是有序的任务，可以分步骤完成的，直接使用串行队列就行。但是如果是一系列并行执行的任务呢？这个时候，就需要dispatch_group帮忙了~总的来说，dispatch_group的使用分如下几步：\n创建dispatch_group_t 添加任务（block） 添加结束任务（如清理操作、通知UI等） 下面着重讲讲在后面两步。\n添加任务 添加任务可以分为以下两种情况：\n自己创建队列：使用dispatch_group_async。 无法直接使用队列变量（如使用AFNetworking添加异步任务）：使用dispatch_group_enter，dispatch_group_leave。 自己创建队列时，当然就用dispatch_group_async函数，简单有效，简单例子如下：\n//省去创建group、queue代码。。。 dispatch_group_async(group, queue, ^{ //Do you work... }); 当你无法直接使用队列变量时，就无法使用dispatch_group_async了，下面以使用AFNetworking时的情况：\nAFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; //Enter group dispatch_group_enter(group); [manager GET:@\u0026#34;http://www.baidu.com\u0026#34; parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { //Deal with result... //Leave group dispatch_group_leave(group); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { //Deal with error... //Leave group dispatch_group_leave(group); }]; //More request... 使用dispatch_group_enter，dispatch_group_leave就可以方便的将一系列网络请求“打包”起来~\n添加结束任务 添加结束任务也可以分为两种情况，如下：\n在当前线程阻塞的同步等待：dispatch_group_wait。 添加一个异步执行的任务作为结束任务：dispatch_group_notify 这两个比较简单，就不再贴代码了=。=\n使用dispatch_barrier_async,dispatch_barrier_sync的注意事项 dispatch_barrier_async的作用就是向某个队列插入一个block，当目前正在执行的block运行完成后，阻塞这个block后面添加的block，只运行这个block直到完成，然后再继续后续的任务，有点“唯我独尊”的感觉=。=\n值得注意的是：\ndispatch_barrier_(a)sync只在自己创建的并发队列上有效，在全局(Global)并发队列、串行队列上，效果跟dispatch_(a)sync效果一样。 既然在串行队列上跟dispatch_(a)sync效果一样，那就要小心别死锁！ dispatch_set_context与dispatch_set_finalizer_f的配合使用 dispatch_set_context可以为队列添加上下文数据，但是因为GCD是C语言接口形式的，所以其context参数类型是“**void ***”。也就是说，我们创建context时有如下几种选择：\n用C语言的malloc创建context数据。 用C++的new创建类对象。 用Objective-C的对象，但是要用__bridge等关键字转为Core Foundation对象。 以上所有创建context的方法都有一个必须的要求，就是都要释放内存！，无论是用free、delete还是CF的CFRelease，我们都要确保在队列不用的时候，释放context的内存，否则就会造成内存泄露。\n所以，使用dispatch_set_context的时候，最好结合dispatch_set_finalizer_f使用，为队列设置“析构函数”，在这个函数里面释放内存，大致如下：\nvoid cleanStaff(void *context) { //释放context的内存！ //CFRelease(context); //free(context); //delete context; } ... //在队列创建后，设置其“析构函数” dispatch_set_finalizer_f(queue, cleanStaff); 详细用法，请看我之前写的Blog为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权\n总结 其实本文更像是总结了GCD中的“坑”=。=\n至于经验，总结一条，就是使用任何技术，都要研究透彻，否则后患无穷啊~\n参考 Grand Central Dispatch (GCD) Reference Concurrency Programming Guide Using Dispatch Groups to Wait for Multiple Web Services ","date":"2015-04-03T00:00:00Z","permalink":"/2015/04/03/something-about-gcd/","title":"GCD使用经验与技巧浅谈"},{"content":"2015-5-28更新 下面评论的好友“@Jim”给了种新的思路，就是在清除context的函数里面，用“_bridge_transfer”转换context，把context的内存管理权限重新交给ARC，这样，就不用显式调用“CFRelease”了。如下：\nvoid cleanStaff(void *context) { //这里用_bridge_transfer转换，将内存管理权限交还给ARC Data *data = (_bridge_transfer Data *)(context); NSLog(@\u0026#34;In clean, context number: %d\u0026#34;, data.number); //不用显式释放context的内存！ } 前言 看过GCD（Grand Central Dispatch）的Apple官方文档的朋友一定见过“dispatch_set_context”和“dispatch_get_context”这两个函数，那么这两个函数该怎么用呢？\n我们都知道，GCD的接口参数都是“C语言类型“的，那么，我们如何将NSObject类型（Foundation框架）的数据，传入GCD的接口呢？（即：Core Foundation和Foundation对象的转换）\n本文关键字 GCD：dispatch_set_context，dispatch_get_context __bridge，__bridge_retained，__bridge_transfer Core Foundation, NSObject dispatch_set(get)_context 先看看这两个函数的原型：\n//设置context void dispatch_set_context (dispatch_object_t object, void *context); //获取context void* dispatch_get_context (dispatch_object_t object); 这里的object一般指的就是通过dispatch_queue_create创建的队列。\n所以，这两个函数分别完成了将context“绑定”到特定GCD队列和从GCD队列获取对应context的任务。\n什么是context 在上述函数原型中，context是一个“void类型指针”，学过C语言的朋友应该都知道，void型指针可以指向任意类型，就是说，context在这里可以是任意类型的指针。\n从这里可以得知，我们可以为队列“set”任意类型的数据，并在合适的时候取出来用。\n用malloc创建context并绑定到队列上 参考Apple官方的例子，我们先用传统的malloc创建context，看看如下简短例子：\n//定义context，即一个结构体 typedef struct _Data { int number; } Data; //定义队列的finalizer函数，用于释放context内存 void cleanStaff(void *context) { NSLog(@\u0026#34;In clean, context number: %d\u0026#34;, ((Data *)context)-\u0026gt;number); //释放，如果是new出来的对象，就要用delete free(context); } - (void)runTest { //创建队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); //创建Data类型context数据并初始化 Data *myData = malloc(sizeof(Data)); myData-\u0026gt;number = 10; //绑定context dispatch_set_context(queue, myData); //设置finalizer函数，用于在队列执行完成后释放对应context内存 dispatch_set_finalizer_f(queue, cleanStaff); dispatch_async(queue, ^{ //获取队列的context数据 Data *data = dispatch_get_context(queue); //打印 NSLog(@\u0026#34;1: context number: %d\u0026#34;, data-\u0026gt;number); //修改context保存的数据 data-\u0026gt;number = 20; }); } 上面的代码运行后如下：\n2015-03-29 20:28:16.854 GCDTest[37787:1443423] 1: context number: 10 2015-03-29 20:28:16.855 GCDTest[37787:1443423] In clean, context number: 20 看，通过为队列设置context，我们就能为队列绑定自定义的数据，然后在合适的时候取出来用。\nNSObject类型的context 在Mac、iOS的开发过程中，我们大部分用的都是Foundation框架下的类，就是如NSString、NSDictionary这些NSObject类型的类。\n但是上面的dispatch_set(get)_context接受的context参数是C语言类型的，即Core Foundation类型的，我们如何转换呢？\n由于ARC不能管理Core Foundation Object的生命周期，所以我们必须先转换context的“类型”，以便转换内存管理权。\n__bridge Apple已经为我们提供了用于转换的关键字，如下：\n__bridge: 只做了类型转换，不修改内存管理权； __bridge_retained（即CFBridgingRetain）转换类型，同时将内存管理权从ARC中移除，后面需要使用CFRelease来释放对象； __bridge_transfer（即CFBridgingRelease）将Core Foundation的对象转换为Objective-C的对象，同时将内存管理权交给ARC。 重新定义context 为了方便下面的说明，我们先定义context类。\n@interface Data : NSObject @property(assign, nonatomic) int number; @end @implementation Data //继承dealloc方法，便于观察对象何时被释放 - (void)dealloc { NSLog(@\u0026#34;Data dealloc...\u0026#34;); } @end 看，我们继承了dealloc方法，这样就能知道Data类型对象什么时候被释放。\n需要注意的点 __bridge的转换是没有转移内存管理权的，这点要特别注意。\n如果在传context对象时，用的是**__bridge**转换，那么context对象的内存管理权还在ARC手里，一旦当前作用域执行完，context就会被释放，而如果队列的任务用了context对象，就会造成“EXC_BAD_ACCESS”崩溃！\n正确的用法 重写上面的例子，如下：\n//定义队列的finalizer函数，用于释放context内存 void cleanStaff(void *context) { //这里用__bridge转换，不改变内存管理权 Data *data = (__bridge Data *)(context); NSLog(@\u0026#34;In clean, context number: %d\u0026#34;, data.number); //释放context的内存！ CFRelease(context); } - (void)testBody { //创建队列 dispatch_queue_t queue = dispatch_queue_create(\u0026#34;me.tutuge.test.gcd\u0026#34;, DISPATCH_QUEUE_SERIAL); //创建Data类型context数据并初始化 Data *myData = [Data new]; myData.number = 10; //绑定context //这里用__bridge_retained转换，将context的内存管理权从ARC移除，交由我们自己手动释放！ dispatch_set_context(queue, (__bridge_retained void *)(myData)); //设置finalizer函数，用于在队列执行完成后释放对应context内存 dispatch_set_finalizer_f(queue, cleanStaff); dispatch_async(queue, ^{ //获取队列的context数据 //这里用__bridge转换，不改变内存管理权 Data *data = (__bridge Data *)(dispatch_get_context(queue)); //打印 NSLog(@\u0026#34;1: context number: %d\u0026#34;, data.number); //修改context保存的数据 data.number = 20; }); } 解释 在dispatch_set_context的时候用**__bridge_retained**转换，将context的内存管理权从ARC移除，交给我们自己管理。 在队列任务中，用dispatch_get_context获取context的时候，用**__bridge**转换，维持context的内存管理权不变，防止出了作用域context被释放。 最后用CFRelease释放context内存。 运行结果 2015-03-29 21:12:41.631 GCDTest[38131:1465900] 1: context number: 10 2015-03-29 21:12:41.632 GCDTest[38131:1465900] In clean, context number: 20 2015-03-29 21:12:41.632 GCDTest[38131:1465900] Data dealloc... 由结果可知，我们的context对象在最后显式调用CFRelease才被释放。\n总结 总的来说，就是合理运用__bridge_retained(transfer)关键字转换对象的内存管理权，让我们自己控制对象的生命周期。\n参考 Grand Central Dispatch (GCD) Reference Concurrency Programming Guide Toll-Free Bridged Types Core Foundation 框架 ","date":"2015-03-29T00:00:00Z","permalink":"/2015/03/29/bind-data-to-gcd-queue/","title":"为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权"},{"content":"前言 Enum，也就是枚举，从C语言开始就有了，C++、Java、Objective-C、Swift这些语言，当然都有对应的枚举类型，功能可能有多有少，但是最核心的还是一个—规范的定义代码中的状态、选项等“常量”。\nItem 5 - Use Enumerations for States, Options, and Status Codes 本节的内容就是如何正确的使用枚举。\n状态与选项的区别（states and options） 在用enum之前，我个人觉得，区分一下状态和选项的概念还是很必要的。\n状态，同时只能有一种，如“OK”，“Error”，不可能同时是OK和Error。 选项，同时可以有一种或一种以上，如App可以同时支持横屏和竖屏，横屏竖屏在这个时候就是“屏幕方向”的两种不同的选项。\n接下来，我们看看如何用枚举定义状态和选项。\nenum与状态（states） 不好的做法 经常看到这样的写法：\n#define STATE_OK 0 #define STATE_ERROR 1 #define STATE_UNKNOW 2 //直接用int型变量接收 int STATE = STATE_UNKNOW; 这样做有如下“不恰当”：\n宏定义没有类型约束，只是单纯的替换。 无法限制状态的所有情况，如，认为的将STATE赋值成3，程序可能就会出错，找不到匹配的状态，因为编译器不会对“STATE = 3;”提出警告。 正确的做法 typedef enum _TTGState { TTGStateOK = 0, TTGStateError, TTGStateUnknow } TTGState; //指明枚举类型 TTGState state = TTGStateOK; 用的时候就如下：\n- (void)dealWithState:(TTGState)state { switch (state) { case TTGStateOK: //... break; case TTGStateError: //... break; case TTGStateUnknow: //... break; } } enum与选项 (options) 选项，就是说一个“选项变量”的类型要能够同时表示一个或多个组合的选择，如下例子：\n//方向，可同时支持一个或多个方向 typedef enum _TTGDirection { TTGDirectionNone = 0, TTGDirectionTop = 1 \u0026lt;\u0026lt; 0, TTGDirectionLeft = 1 \u0026lt;\u0026lt; 1, TTGDirectionRight = 1 \u0026lt;\u0026lt; 2, TTGDirectionBottom = 1 \u0026lt;\u0026lt; 3 } TTGDirection; 看，这里的选项是用位运算的方式定义的，这样的好处就是，我们的选项变量可以如下表示：\n//用“或”运算同时赋值多个选项 TTGDirection direction = TTGDirectionTop | TTGDirectionLeft | TTGDirectionBottom; //用“与”运算取出对应位 if (direction \u0026amp; TTGDirectionTop) { NSLog(@\u0026#34;top\u0026#34;); } if (direction \u0026amp; TTGDirectionLeft) { NSLog(@\u0026#34;left\u0026#34;); } if (direction \u0026amp; TTGDirectionRight) { NSLog(@\u0026#34;right\u0026#34;); } if (direction \u0026amp; TTGDirectionBottom) { NSLog(@\u0026#34;bottom\u0026#34;); } direction变量的实际内存如下：\n这样，用位运算，就可以同时支持多个值。\nenum在Objective-C中的“升级版” 一般来说，我们不能指定枚举变量的实际类型是什么，就是说，我们不知道枚举最后是int型，还是其他的什么类型。但是从C++ 11开始，我们可以为枚举指定其实际的存储类型，如下语法：\nenum TTGState : NSInteger {/*...*/}; 但是，我们在定义枚举的时候如何保证兼容性呢？Foundation框架已经为我们提供了更加“统一、便捷”的枚举定义方法，我们重新定义上面的例子：\n//NS_ENUM，定义状态等普通枚举 typedef NS_ENUM(NSUInteger, TTGState) { TTGStateOK = 0, TTGStateError, TTGStateUnknow }; //NS_OPTIONS，定义选项 typedef NS_OPTIONS(NSUInteger, TTGDirection) { TTGDirectionNone = 0, TTGDirectionTop = 1 \u0026lt;\u0026lt; 0, TTGDirectionLeft = 1 \u0026lt;\u0026lt; 1, TTGDirectionRight = 1 \u0026lt;\u0026lt; 2, TTGDirectionBottom = 1 \u0026lt;\u0026lt; 3 }; 所以，在开发Mac、iOS程序中，最好所有的枚举都用“NS_ENUM”和“NS_OPTIONS”定义，保证统一。\n总结 充分的用好枚举，可以增强代码的可读性，减少各种“错误”，让代码更加的规范。\n","date":"2015-03-21T00:00:00Z","image":"/assets/2015/03/21/effective-objective-c-5-enum/blog_effective_objective_c_5_enum_2.jpg","permalink":"/2015/03/21/effective-objective-c-5-enum/","title":"Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5"},{"content":"前言 开发过iOS、Mac的朋友应该对“@autoreleasepool”不陌生。只要在Xcode里创建一个工程，就能看到下面这样的代码：\n//iOS program int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } //Command line program int main(int argc, const char *argv[]) { @autoreleasepool { //... } return 0; } 看，每个main函数的主体都被**@autoreleasepool**的Block块包在里面，也就是说，接下来所有的对象创建都在这个block里面。\n那么，@autoreleasepool的作用到底是什么呢？我们开发中可以用它来做什么呢？\n可以在某些情况下，大幅度降低程序的内存占用，如下图:\n测试的内容：500000次循环，每次循环创建一个NSNumber实例和两个NSString实例。 图：红线表示没有用**@autoreleasepool**时的内存占用。 图：绿线表示用了**@autoreleasepool**优化后的内存占用！ 效果是不是很明显！\n代码Github地址：AutoReleasePoolTestExample Xcode 6, iOS 8, iPhone 5模拟器.\nMRC 与 ARC MRC（Mannul Reference Counting）和ARC(Automatic Reference Counting)，分别对应着手动引用计数和自动引用计数。\n对！是计数，不是“GC、垃圾回收”什么的，就是说，在Objective-C的开发中，ARC不代表像Java那样有GC做垃圾回收，所以本质上还是要“手动”管理内存的。也就是说，我们在ARC环境下写的代码，不用自己手动插入“retain、release这些消息”，ARC会在编译时为我们在合适的位置插入，释放不必要的内存。\n而**@autoreleasepool就跟对象的release**密切相关。\n@autoreleasepool 干了啥 在MRC时代，如果我们想先retain一个对象，但是并不知道在什么时候可以release它，我们可以像下面这么做：\nNSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSString* str = [[[NSString alloc] initWithString:@\u0026#34;tutuge\u0026#34;] autorelease]; //use str... [pool release]; //str is released 就是说，我们可以在创建对象的时候给对象发送“autorelease”消息，然后当NSAutoreleasePool结束的时候，“标记过”autorelease的对象都会被“release”掉，也就是会被释放掉。\n但是在ARC时代，我们不用手动发送autorelease消息，ARC会自动帮我们加。而这个时候，@autoreleasepool做的事情，跟NSAutoreleasePool就一模一样了。\n什么时候用@autoreleasepool 根据Apple的文档，使用场景如下：\n写基于命令行的的程序时，就是没有UI框架，如AppKit等Cocoa框架时。 写循环，循环里面包含了大量临时创建的对象。（本文的例子） 创建了新的线程。（非Cocoa程序创建线程时才需要） 长时间在后台运行的任务。 利用@autoreleasepool优化循环 利用@autoreleasepool优化循环的内存占用，我觉得最有用的一点，下面就说说这个点。 如下面的循环，次数非常多，而且循环体里面的对象都是临时创建使用的，就可以用**@autoreleasepool**包起来，让每次循环结束时，可以及时的释放临时对象的内存。\n//来自Apple文档，见参考 NSArray *urls = \u0026lt;# An array of file URLs #\u0026gt;; for (NSURL *url in urls) { @autoreleasepool { NSError *error; NSString *fileContents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:\u0026amp;error]; /* Process the string, creating and autoreleasing more objects. */ } } 这么做的效果是极其显著地，就如本文最开始的图一样，可以自己把示例工程下回来运行下试试~\n总结 @autoreleasepool看起来很不“起眼”，平常开发很容易就忽略它了，但是仔细一看，确如此有用~\n参考 Using Autorelease Pool Blocks What is autoreleasepool? - Objective-C Objective-C: Why is autorelease (@autoreleasepool) still needed with ARC? What-is-autorelease-pool? NSAutoreleasePool 示例中的第三方库 PNChart\n运行中用代码获取App运行消耗内存 Programmatically retrieve memory usage on iPhone\n","date":"2015-03-17T00:00:00Z","permalink":"/2015/03/17/what-is-autoreleasepool/","title":"@autoreleasepool-内存的分配与释放"},{"content":"前言 宏定义、#define啥的，我们经常遇到。 一般来说，最常用的可能就是定义一些常量、简单的“函数”，如下：\n//定义常量PI #define PI 3.1415926 //定义“函数”MIN #define MIN(A,B) ((A) \u0026lt; (B) ? (A) : (B)) 但是，这样定义常量、函数，有一定的风险。（见：Effective-Objective-C-读书笔记-Item-4-如何正确定义常量, 宏定义的黑魔法 - 宏菜鸟起飞手册）\n本文就列出几条我个人在iOS开发当中常用的经验与技巧。\n简化代码 在开发当中，我们可能会大量的写一些重复的，甚至具有“危险”的代码，用宏定义#define，往往可以简化代码，看看下面几个例子。\nGCD与#define dispatch_once 项目中往往少不了各种单例类，我们一般会如下定义：\n+ (instancetype)sharedService { static XXX *sharedService = nil; static dispatch_once_t onceToken; dispatch_once, ^{ sharedService = [XXX new]; }); return sharedService; } 这时候，就要注意几点：\ndispatch_once_t必须是static的，否则会各种报错=。=。 dispatch_once的第一个参数必须取onceToken的地址，就是要“\u0026amp;onceToken。” 看，写个小小的单例都要这么注意，错了一点都很危险啊，而且总要重复写这几行。所以这个时候需要让宏定义帮忙：\n//定义宏定义 #define DISPATCH_ONCE_BLOCK(onceBlock) static dispatch_once_t onceToken; dispatch_once(\u0026amp;onceToken, onceBlock); + (instancetype)sharedService { static XXX *sharedService = nil; //直接一行搞定 DISPATCH_ONCE_BLOCK(^{ sharedService = [XXX new]; }) return sharedService; } 看，是不是变得更加简洁、“安全了”~\ndispatch_async 除了dispatch_once，另一个常用的就是dispatch_async。不多说，直接看例子：\n//在Main线程上运行 #define DISPATCH_ON_MAIN_THREAD(mainQueueBlock) dispatch_async(dispatch_get_main_queue(), mainQueueBlock); //在Global Queue上运行 #define DISPATCH_ON_GLOBAL_QUEUE_HIGH(globalQueueBlocl) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), globalQueueBlocl); #define DISPATCH_ON_GLOBAL_QUEUE_DEFAULT(globalQueueBlocl) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), globalQueueBlocl); #define DISPATCH_ON_GLOBAL_QUEUE_LOW(globalQueueBlocl) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), globalQueueBlocl); #define DISPATCH_ON_GLOBAL_QUEUE_BACKGROUND(globalQueueBlocl) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), globalQueueBlocl); //主线程上 DISPATCH_ON_MAIN_THREAD(^{ //更新UI }) //Global Queue DISPATCH_ON_GLOBAL_QUEUE_DEFAULT(^{ //异步耗时任务 }) 再也不用写一大堆”dispatch_async(dispatch_get_global_queue”了看起来是不是很清爽\n能一行搞定，干嘛还要写多行 很多时候，我们写的函数、方法都会在起始的地方加一些公共的判断，条件满足了才可以继续往下运行，如“用户是否登录”、“某个模块是否加载完成”等等，所以，我们可能会写如下样子的代码：\n- (void)doSomething { if (![[XXXAccountService sharedService] isLogin]) { return; } //do staff... } - (void)doSomething2 { if (![[XXXAccountService sharedService] isLogin]) { return; } //do staff... } 每次都要这么写，是不是感觉很浪费时间？而且重复的好多=。= 所以，我们要用#define，把3行变成一行:\n//定义 #define CHECK_LOGIN_IN if (![[XXXAccountService sharedService] isLogin]) {return;} - (void)doSomething { CHECK_LOGIN_IN //do staff... } - (void)doSomething2 { CHECK_LOGIN_IN //do staff... } 其实#define的原理就是不管三七二十一，直接做替换，所以我们完全可以利用这个特点，发挥自己的想象，简化代码~\n宏定义与Log 打Log，是个永恒的话题。\nNSLog增强版 我们会经常用到NSLog打Log，但是，这个函数能提供的信息非常少，所以，下面就是一个增强版, 最好定义在项目的pch文件中：\n#define NSLog(format, ...) \\ do { \\ NSLog(@\u0026#34;\u0026lt;%@ : %d : %s\u0026gt;-: %@\u0026#34;, \\ [[NSString stringWithUTF8String:__FILE__] lastPathComponent], \\ __LINE__, \\ __FUNCTION__, \\ [NSString stringWithFormat:format, ##__VA_ARGS__]); \\ } while(0) 使用：\nint main(int argc, const char *argv[]) { @autoreleasepool { NSLog(@\u0026#34;Blog: %@\u0026#34;, @\u0026#34;http://tutuge.me\u0026#34;); } return 0; } 输出：\n\u0026lt;main.m : 22 : main\u0026gt;-: Blog: http://tutuge.me 看，这下Log就带上了文件名、行号、函数名，一下子就清楚多了~ 至于为什么要用这个奇怪的“do{…}while(0)”，其实就是为了防止在不同的使用场景中导致语法错误，详细请见：宏定义的黑魔法 - 宏菜鸟起飞手册\n为Log增加全局开关 开发中，我们往往会在许多地方加上Log，但是在发布的时候，又不想显示这些Log，这个时候难道要一个一个的删除打Log的地方？ 所以，为了增加开关，我们需要将上面的增强型NSLog改造一下：\n//判断是否要Log #ifdef NEED_DEBUG #define NSLog(format, ...) \\ //Log定义... #else #define NSLog(format, ...) do{ } while(0) #endif 所以，当我们需要Log的时候，在上面定义的之前加上下面这一行：\n#define NEED_DEBUG 就可以打开Log了。不加的话，就不会有Log简单的条件编译\n为Log增加等级 开发过Android的应该都知道，Android里面可以很方便的用Log.i、Log.d输出不同“等级”的Log，调试更加方便，NSLog却不行。所以，还是要借助宏定义：\n//Debug等级Log, 在此之前定义自己的NSLog #ifdef LOG_LEVEL_DEBUG #define DLog(format, ...) NSLog(@\u0026#34;\u0026lt;DEBUG\u0026gt;: %@\u0026#34;, [NSString stringWithFormat:format, ##__VA_ARGS__]) #else #define DLog(format, ...) do{ } while(0) #endif //Info等级Log #ifdef LOG_LEVEL_INFO #define ILog(format, ...) NSLog(@\u0026#34;\u0026lt;Info\u0026gt;: %@\u0026#34;, [NSString stringWithFormat:format, ##__VA_ARGS__]) #else #define ILog(format, ...) do{ } while(0) #endif //Error等级Log #ifdef LOG_LEVEL_ERROR #define ELog(format, ...) NSLog(@\u0026#34;\u0026lt;Error\u0026gt;: %@\u0026#34;, [NSString stringWithFormat:format, ##__VA_ARGS__]) #else #define ELog(format, ...) do{ } while(0) #endif 当我们在打Log的时候，就可以根据需要，用不同等级的Log，而且可以方便的定制需要输出哪几种，并且之前的“增强版”NSLog也可以使用。\n完整的请看Gist.\n其它 还有一些简单好用的宏定义，在这就只举几个典型的例子了~\n打印自定义类型信息 假如有如下结构体定义(自定义类的话，直接重写description方法即可):\n//复数结构体 typedef struct Complex { float r; //实部 float i; //虚部 } Complex; //定义打印Complex类型的宏 //输出：(1+2i), (1-2i) #define NSLogComplex(complex) NSLog(@\u0026#34;Complex: (%g%s%gi)\u0026#34;, complex.r, (complex.i \u0026gt; 0 ? \u0026#34;+\u0026#34; : \u0026#34;\u0026#34;), complex.i) 常用函数简化、单例获取 //获取View的属性 #define GetViewWidth(view) view.frame.size.width #define GetViewHeight(view) view.frame.size.height #define GetViewX(view) view.frame.origin.x #define GetViewY(view) view.frame.origin.y //屏幕常量 #define GetScreenWidth [[UIScreen mainScreen] bounds].size.width #define GetScreenHeight [[UIScreen mainScreen] bounds].size.height //获取图片资源 #define GetImage(imageName) [UIImage imageNamed:[NSString stringWithFormat:@\u0026#34;%@\u0026#34;,imageName]] //等等~只要你想得到~ 总结 宏定义的各种Tip还远远不止这些，合理使用它，可以有效改善代码的阅读质量~\n参考 The C Preprocessor 宏定义的黑魔法 - 宏菜鸟起飞手册 ","date":"2015-03-15T00:00:00Z","permalink":"/2015/03/15/%E6%9C%89%E5%85%B3%E5%AE%8F%E5%AE%9A%E4%B9%89%E7%9A%84%E7%BB%8F%E9%AA%8C%E4%B8%8E%E6%8A%80%E5%B7%A7-%E7%AE%80%E5%8C%96%E4%BB%A3%E7%A0%81-%E5%A2%9E%E5%BC%BALog/","title":"有关宏定义的经验与技巧-简化代码-增强Log"},{"content":"前言 写博客果然比光看书要花时间=。=，书都快看完了，才写到第4节。但是总结一遍，的确印象深刻啊~~\n这次的主题是“常量”，非常常用，又非常重要的东西~良好的代码规范、设计，少不了各种常量。\nItem 4 - Prefer Typed Constants to Preprocessor #define 本节着重对比了常量的定义形式，类型常量和#define的区别、各自的特点。最后得出，我们应该用带有类型的常量，而不是#define。\n一个简单的示例 应用中，少不了各种动画，也就少不了设定动画的时间长度，一般来说，我们会设定短、中、长几种时间常量，用于不同的场合，了解C语言的童鞋估计对“#define”宏定义不陌生，于是，可能就会如下这么定义：\n#define ANIMATION_DURATION_SHORT 0.3 #define ANIMATION_DURATION_MIDDLE 0.6 #define ANIMATION_DURATION_LONG 1.0 于是，所有用到动画时长的地方，直接用这几个宏定义就行了。\n乍一看，似乎没什么问题。\n类型+不可变 先来看看宏定义干了什么。 总的来说，#define就是“替换”，在编译时，将一切宏定义替换成定义后面的内容。（参考百度百科）\n关键在于：\n宏定义不会检查替换的“类型”，只要遇到，就做替换。 宏定义不保证“不可变”，因为可以重复定义，不能保证值不变。(这个我觉得是最容易让人忽略的地方) 所以，用宏定义定义常量，很“危险”。\n正确的做法 恰当的常量，应该具备合适的常量名字、正确的类型，还有const、extern等关键字的修饰，当然，还要根据常量适用的“范围”，做出恰当的处理。一下举例说明。\n只在函数中用的常量 只在函数中用的常量：\n- (void)runAnimation { //动画时间长度常量 static const NSTimeInterval kAnimationDurationLong = 1.0f; //... } static、const定义了它是静态不可变的；NSTimeInterval则“精确”指定了常量的类型。\n只在一个“.m”文件中使用的常量 在类的实现文件“xxx.m”文件中定义常量，也是很常见的。\n你可能会觉得，既然是一个类使用的，那就放在类的头文件里面定义不就好了。\n但是这么想是错的=。=\n因为所有要用到这个类的文件，都会无形当中包含了这些常量声明，将不必要的数据暴露在外面，这样是违反设计原则的，并且，如果常量名字取得不恰当，还有可能造成冲突。\n所以，应该如下:\n//XXX.m //必须要static static const NSTimeInterval kAnimationDurationLong = 1.0f; @implementation XXX //... @end 需要注意的点：\n一定要放在.m文件中。 必须是static，表示此常量作用域只在此.m文件中。如果不加static的话，常量就会在全局作用域中声明，可能会导致跟其他的常量冲突。 const，当然。 NSTimeInterval，确定的类型。 kAnimationDurationLong，“k”开头的命名，也算是Apple的规范了吧，内部常量统一以“k”开头。 全局常量 一般来说，最常见的全局常量就是发广播Notification时用的Notification的名字，往往都是一些字符串，下面就以这个为例子：\n假设我们的类叫“TTGClass”，我们的类会向外发送Notification，所以需要定义一个Notification的名字常量供外界注册，如下：\n//TTGClass.h extern NSString * const TTGClassWorkBeginNotification; //TTGClass.m NSString * const TTGClassWorkBeginNotification = @\u0026#34;TTGClassWorkBeginNotification\u0026#34;; 关键：\n头文件中只是做声明，不做定义。所以用extern表示常量在别处定义的，尽量隐藏细节。 同类名做常量名字的开头，这个非常关键，因为Objective-C里面没有像Java那样的“包”机制，所以必须靠命名来划分不同的类，常量当然也要遵循这个规则。 NSString，确定的类型；const，表明常量；Notification后缀，表明用途。这些都是需要注意的。 总结 良好的代码，总是少不了定义各种常量，以避免“魔鬼数字、魔鬼字符串”。所以我们应该尽量避免少用#define定义常量，而是用带有类型的、const的常量，并且尽可能的缩小常量的作用域。\n暴露的细节越少，出错的机会也就越小（忘了从哪里看到的=。=）\n","date":"2015-03-11T00:00:00Z","permalink":"/2015/03/11/Effective-Objective-C-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-Item-4-%E5%A6%82%E4%BD%95%E6%AD%A3%E7%A1%AE%E5%AE%9A%E4%B9%89%E5%B8%B8%E9%87%8F/","title":"Effective-Objective-C-读书笔记-Item-4-如何正确定义常量"},{"content":"前言 在iOS开发中，经常需要用UITextView作为编辑文本的输入控件。 但是如何在编辑时插入自定义表情呢？就是像发微博时那样？\n本文简单的用NSTextAttachment、NSAttributedString的特性，实现了\n在UITextView中编辑文字时插入自定义表情图片 同时可以返回带有表情“替换符”的纯文本字符串。 示例 本文代码工程地址：https://github.com/zekunyan/UITextViewDIYEmojiExample\n效果图：\n背景知识 NSAttributedString及其子类，用于显示富文本。 NSTextAttachment，NSAttributedString的一种样式类，可以在文本中显示图片。 NSTextStorage，UITextView中的实际的文本封装。（见参考中的UITextView文档） 表情与其标志 首先需要明确的是，我们的自定义表情一定是有一一对应的“标志”的，如“[/emoji_haha]”。\n就是说，为了方便处理，方便在数据库、网络传输中保存、传输带有表情图片的文本，我们必须要为每种表情取特定的“名字”，数据库中储存的、网络传输的文本就只包含这些标志名字就行，在显示的时候做对应的替换。\n如：\ntutuge.me\n对应的纯文本就是：\ntutuge.me[/emoji_1]\n插入并显示表情图片 插入表情很简单，直接实例化NSTextAttachment类，将需要的表情的UIImage实例赋值给NSTextAttachment的image属性，然后用“[NSAttributedString attributedStringWithAttachment:]”方法实例化一个NSAttributedString的对象，插入到UITextView的textStorage对应的位置即可。\n如下：\nNSTextAttachment *emojiTextAttachment = [NSTextAttachment new]; //设置表情图片 emojiTextAttachment.image = emojiImage; //插入表情 [textView.textStorage insertAttributedString:[NSAttributedString attributedStringWithAttachment:emojiTextAttachment] atIndex:textView.selectedRange.location]; 这样，就能在UITextView当前光标位置插入表情图片了。\n获取带有表情标志的文本字符串 难点 NSTextAttachment被插入到NSAttributedString中的时候，就被当成了一个字符处理！！！。 就是说，只从UITextView的text中，是找不回文本里面不同的表情所对应的标志的！\n解决点 我们要能遍历出当前文本中所有的表情，也就是NSTextAttachment类。 我们要能知道遍历出的表情，对应的标志是什么。 遍历所有的NSTextAttachment类属性 遍历，嗯，先看看Apple有没有提供相应的方法，能遍历NSAttributedString（及其子类）的属性的。查阅文档：NSAttributedString Class Reference，可以找到这么一个方法：“- enumerateAttribute:inRange:options:usingBlock:”，其原型如下：\n- (void)enumerateAttribute:(NSString *)attrName inRange:(NSRange)enumerationRange options:(NSAttributedStringEnumerationOptions)opts usingBlock:(void (^)(id value, NSRange range, BOOL *stop))block 用处：\nExecutes the Block for the specified attribute run in the specified range. 看，就是这个方法就能遍历出NSTextAttachment对象了\n创建NSTextAttachment的子类 如何绑定NSTextAttachment所表示的表情和与其对应的标志？创建子类嘛~直接在子类中增加属性，保存标志不就行了。 如下：\n@interface EmojiTextAttachment : NSTextAttachment @property(strong, nonatomic) NSString *emojiTag; @end 所以，这个时候，插入表情的代码应该就是下面这样：\nEmojiTextAttachment *emojiTextAttachment = [EmojiTextAttachment new]; //保存表情标志 emojiTextAttachment.emojiTag = emojiTag; //设置表情图片 emojiTextAttachment.image = emojiImage; //插入表情 [textView.textStorage insertAttributedString:[NSAttributedString attributedStringWithAttachment:emojiTextAttachment] atIndex:textView.selectedRange.location]; 创建NSAttributedString的Category 最后，就是将这个遍历表情、拼接最终文本字符串的方法设置成NSAttributedString的自定义Category方法，以方便直接调用。 当然，这里面有些细节的处理，如替换表情标志时的字符串偏移量计算等，看代码吧。\n如下：\n//NSAttributedString+EmojiExtension.h @interface NSAttributedString (EmojiExtension) - (NSString *)getPlainString; @end //NSAttributedString+EmojiExtension.m @implementation NSAttributedString (EmojiExtension) - (NSString *)getPlainString { //最终纯文本 NSMutableString *plainString = [NSMutableString stringWithString:self.string]; //替换下标的偏移量 __block NSUInteger base = 0; //遍历 [self enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, self.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { //检查类型是否是自定义NSTextAttachment类 if (value \u0026amp;\u0026amp; [value isKindOfClass:[EmojiTextAttachment class]]) { //替换 [plainString replaceCharactersInRange:NSMakeRange(range.location + base, range.length) withString:((EmojiTextAttachment *) value).emojiTag]; //增加偏移量 base += ((EmojiTextAttachment *) value).emojiTag.length - 1; } }]; return plainString; } @end 使用 直接调用getPlainString方法即可。\n总结 其实本文也是来源于最近的项目需求，在网上一直找不到比较好的解决方案，就自己摸索出来一个。至于复杂的图文混合编辑，当然还是Core Text来的强大（自己也在学习中）~\n如果有更好地办法，一定要告诉我啊~~~\n参考 UITextView Class Reference NSAttributedString Class Reference ","date":"2015-03-07T00:00:00Z","image":"/assets/2015/03/07/UITextView编辑时插入自定义表情-简单的图文混编/inputEmoji_Sample.gif","permalink":"/2015/03/07/UITextView%E7%BC%96%E8%BE%91%E6%97%B6%E6%8F%92%E5%85%A5%E8%87%AA%E5%AE%9A%E4%B9%89%E8%A1%A8%E6%83%85-%E7%AE%80%E5%8D%95%E7%9A%84%E5%9B%BE%E6%96%87%E6%B7%B7%E7%BC%96/","title":"UITextView编辑时插入自定义表情-简单的图文混编"},{"content":"前言 经常都能在一些博客、文档中看到“Model”、“Entity”的字样。如MVC模式中的“M就是Model”，“MVVM对应的Model-View-ViewModel”，数据库抽象层里面的“Model“等。\n它们的区别到底是什么？\n其实没有真正官方的对Entity和Model的定义，在此我只是说说我自己的理解。\n什么是Entity 先看看这个单词是什么意思： 爱词霸：n: 实体，实际存在物，本质。\n在代码中，我们经常会遇到一些具有“现实”意义的“个体”，如一个用户，一首歌曲，一条评论等等，它们都是在逻辑上完整的一个整体，都具有相似的结构，但是却因为“属性”的不同而不同。\n看看一个典型的Entity类：\n//Java //Entity类定义 class UserEntity { public String name; public int age; public String address; } //Entity对象 UserEntity john = new UserEntity(); john.name = \u0026#34;John\u0026#34;; john.age = 20; john.address = \u0026#34;China Hubei Wuhan\u0026#34;; UserEntity tom = new UserEntity(); tom.name = \u0026#34;Tom\u0026#34;; tom.age = 30; tom.address = \u0026#34;USA Californian\u0026#34;; 所以：\nEntity类，或者说实体类，代表了一种“个体”，类中的属性（就是各种变量）则是这个“个体”的具体体现。如UserEntity类，里面有name、age属性，就是说Entity类“描述”了某种个体。 Entity类的具体对象，代表了一个具体的实体，一般来说，在代码当中，一个具体的Entity类对象所代表的实体应当是唯一的。 往往Entity类的定义跟数据库中的表定义是一一对应的。 而Entity的具体对象往往对应数据库中的一行数据。 Entity是代码中操作数据的“最小单元”。 什么是Model 各种用法 其实很多时候，Model有如下几种用法：\n当成Entity用，完全不具备其他业务逻辑代码。 当成数据库接口层，就是将数据库的读写，增删改查都封装在了Model中，往往就会有如“getUserByID”这样的方法。 当成纯粹的业务处理类。 所以仁者见仁，智者见智，其实都可以。\n在此说说我的认识。\n我的认识 先看看Model单词的意思: 爱词霸：n: 模型；模式；典型。\n既然是模型，而不是“数据”，所以我觉得，Model应该是利用Entity实体，来组合、处理、整合更上层、更抽象数据的类。\n还是基于上面的“UserEntity”，看看下面的代码：\nclass UserListModel { private UserEntity[] users; //构造函数，初始化用户列表等。 UserEntity getUserByName(String name) { //查找... } void transformUserAddressFormat { //转化地址格式 } } 就是说，Entity还是原本的概念，只不过Model是基于Entity的更高层的数据抽象与处理。\n总结 说实话，Entity、Model，它们的职责到底是什么，真的不好说明白，大家也就做个参考吧=。=\n参考 DDD and MVC: Difference between ‘Model’ and ‘Entity’ Whatis.com Entity Models vs. Entities ","date":"2015-03-01T00:00:00Z","permalink":"/2015/03/01/Entity%E5%92%8CModel%E7%9A%84%E4%B8%8D%E5%90%8C-%E5%85%B3%E4%BA%8E%E4%BB%A3%E7%A0%81%E7%9A%84%E6%95%B0%E6%8D%AE%E5%B1%82/","title":"Entity和Model的不同-关于代码的数据层"},{"content":"前言 项目最近一次审核被拒了=。= 仔细看了一下后，发现是没有遵守“iOS Data Storage Guidelines”。 在此总结一下。\n被拒原因 先看看被拒的反馈信息：\nApps must follow the iOS Data Storage Guidelines or they will be rejected On launch and content download, your app stores 12.14 MB, which does not comply with the iOS Data Storage Guidelines. 大致意思就是App下载东西以后，在iCloud里面查看此App需要备份的体积变大了。\n再根据 iOS Data Storage Guidelines的说明，得出几点重要信息：\n只有用户创建的，并且不能被App重现的文档、数据，才应该被保存在“\u0026lt;Application_Home\u0026gt;/Documents”路径下。 可以重新产生、下载的数据可以放在“\u0026lt;Application_Home\u0026gt;/Library/Caches”目录下，如用于缓存的数据库文件。 纯粹用于缓存的数据可以放在“\u0026lt;Application_Home\u0026gt;/tmp”目录下。 可以通过对NSURL加参数，来保证特定的文件夹及其内容不被iCloud备份，也不会被清除，如Document文件夹。 所以可以初步得出被拒的原因是因为App中下载的文件也被包含在iCloud备份当中了，而Apple审核人员觉得这些文件不应该被备份。\n真正应该备份的数据 所以，真正需要备份的文件是用户创建的，不能通过程序重新产生的，如记事本应用中用户创建的文本数据，绘画应用中用户画的画等。\n解决当中遇到的坑 既然知道了原因，那就好办了。\n根据iOS Data Storage Guidelines的简短说明，乍一看，\u0026lt;Application_Home\u0026gt;/Library/Caches是存放可以重新产生、下载的数据。\n但是有个“陷阱”在这。\n系统可能清空的目录 根据官方文档“File System Programming Guide”可以整理出一下两个系统可能会清除的目录：（在储存空间不够时）\nLibrary/Caches/ tmp/ 所以，像App下载的歌曲等资源文件是不能放在上面两个目录中的。\n解决： 所以，为了保险起见，最靠谱的还是放在**\u0026lt;Application_Home\u0026gt;/Documents**中，并且通过设置，不让iCloud备份。\nApple已经提供了相应的函数，如下（iOS 5.1及以后的版本）：\n- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL { assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]); NSError *error = nil; BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES] forKey: NSURLIsExcludedFromBackupKey error: \u0026amp;error]; if(!success){ NSLog(@\u0026#34;Error excluding %@ from backup %@\u0026#34;, [URL lastPathComponent], error); } return success; } 特性 \u0026lt;Application_Home\u0026gt;/Documents根目录也可以通过上面的函数设置成不备份。（如应用AVPlayer） 设置是递归的，所以其所有子目录和文件都不用再设置一次，都会自动不被备份。（测试有效，但是不确定=。=，参考：is-nsurlisexcludedfrombackupkey-recursive） 总结 多看官方说明=。=\n参考 iOS Data Storage Guidelines How do I prevent files from being backed up to iCloud and iTunes? File System Programming Guide ","date":"2015-02-28T00:00:00Z","permalink":"/2015/02/28/%E4%B8%80%E6%AC%A1%E5%AE%A1%E6%A0%B8%E8%A2%AB%E6%8B%92%E7%9A%84%E7%BB%8F%E5%8E%86-%E5%85%B3%E4%BA%8EiCloud%E5%88%B0%E5%BA%95%E5%BA%94%E8%AF%A5%E5%A4%87%E4%BB%BD%E4%BB%80%E4%B9%88%E6%95%B0%E6%8D%AE/","title":"一次审核被拒的经历-关于iCloud到底应该备份什么数据"},{"content":"前言 Objective-C中的Blocks实在是太好用了，有了它，我们就可以将“代码块”封装起来，当成变量使用、传递，所以，在Objective-C中，iOS中，许多的回调是用Block来完成的。\n问题 我自己在之前的项目中，就遇到过一个问题。因为项目的需要，手机端所有的跟服务器的通信都需要“加密”，在经过一番调研后，我们使用的是类似OAuth机制（至于什么是OAuth，各位Google一下就明白了，大部分的公共API都用到了这种机制）。\n缓存Http请求后的回调代码块 既然是OAuth机制，就是说我们每次向服务器发起的请求，除了跟业务相关的参数，还需要带上AccessToken、UserToken这类的授权参数，而AccessToken在本次请求中是否会过期，在结果返回前是不知道的，所以，为了更好地用户体验，不至于让用户在某个时刻的操作，因为授权过期了而“没有响应”，我们的每次Http请求都必须缓存起来。\n这里的缓存可不是单单指缓存请求的参数、URL，更重要的是缓存Http请求成功或者失败时的“回调代码块”。\n实现 AFNetworking的Get方法 在iOS的项目中，AFNetworking应该算是使用的最多的网络库了，它的Get请求的方法接口如下：\n- (AFHTTPRequestOperation *)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure 只有四个参数，分别是Url、参数、成功的回调Block，失败的回调Block，看，这里，回调的代码块就被封装到了Block里面，并当成参数传入。\n再次封装 介于AFNetworking只提供了最基本的Get、Post、Put这些方法，所以，我们往往根据业务的接口做了封装，如先定义了请求的类型枚举：\ntypedef NS_ENUM(NSUInteger, HTTP_REQUEST_TYPE) { ICG_GET, ICG_POST }; 然后定义了统一的请求入口与回调参数Block：\n//请求成功后的回调 typedef void (^http_success)(id result); //请求失败后的回调 typedef void (^http_fail)(); //统一的请求入口 + (void)executeWithType:(HTTP_REQUEST_TYPE)type api:(NSString *)api parameters:(NSDictionary *)parameters success:(http_success)success fail:(http_fail)fail { //为每个请求增加AccessToken、UserToken等公共参数 //... //缓存Http请求 //... //根据type区分具体是哪一种Http请求。 switch (type) { case ICG_GET: [HttpUtil get:url parameters:paras success:success fail:fail]; break; case ICG_POST: [HttpUtil post:url parameters:paras success:success fail:fail]; break; default: break; } } 再次封装的好处 为什么要再次做封装，将基本的Http请求统一入口？道理很简单，就是为了能方便的对每次请求增加额外的处理，并且对上层调用透明。如:\n统一为每个请求增加Log。 统一为每个请求增加如AccessToken这些额外的参数。 方便对Http请求做缓存！ 缓存Http请求的数据结构 既然我们已经有了统一的请求入口，为了缓存，所需要做的就是将能代表一次请求的所有参数缓存起来就行了，如URL、请求参数等，如下数据结构：\n@interface RequestHolder : NSObject //URL地址 @property(strong, nonatomic) NSString *url; //请求参数 @property(strong, nonatomic) NSMutableDictionary *paras; //请求类型，如Get、Post @property(assign, nonatomic) HTTP_REQUEST_TYPE type; //请求成功回调 @property(copy) http_success success; //请求失败回调 @property(copy) http_fail fail; @end 这几个参数，就可以完整的“代表、重现”一次具体的Http请求。\n如何缓存 只要App开启，Http请求就会不停地产生，会有源源不断的请求，难道我们把每次请求都缓存起来？\n全部都缓存肯定是不行的，所以我们需要一个缓存策略。\n一个App中，所有的Http请求的路径，也就是Url，不包括参数，应该是数量固定的，如“/user, /comment”，并且，一个请求往往对应的操作、功能也是唯一的，所以，完全可以以请求的Url为Key，以本次请求的“RequestHolder”为Value，用Objective-C中的NSDictionary来缓存Http请求。\n所以，缓存请求的方法大致如下：\n//缓存Http请求 - (void)cacheHttpRequest:(HTTP_REQUEST_TYPE)type url:(NSString *)url parameters:(NSMutableDictionary *)parameters success:(http_success)success fail:(http_fail)fail { //创建缓存数据结构，保存本次请求的参数、回调 ICGRequestHolder *requestHolder = [ICGRequestHolder new]; requestHolder.url = url; requestHolder.paras = parameters; requestHolder.success = [success copy]; requestHolder.fail = [fail copy]; requestHolder.type = type; //保存本次请求 [_operationQueue addOperationWithBlock:^{ _cachedRequests[url] = requestHolder; }]; } 这样，就能完整的缓存Http请求了。\n从缓存中恢复Http请求 恢复请求很简单，直接遍历所有缓存的请求，再次发起Http请求即可，就不再贴代码了。\n删除成功的Http请求 缓存的目的是在请求失败时，如AccessToken超时时，我们可以在重新获取AccessToken后重现上次失败的请求，所以，一旦请求成功时，我们还需要将成功的请求删除，当然，也是利用Url，如下：\n- (void)removeRequestForKey:(NSString *)url { //放入队列执行 [_operationQueue addOperationWithBlock:^{ //删除Url对应的请求 [_cachedRequests removeObjectForKey:url]; }]; } 总结 本文只是简单地利用Objective-C中的Block特性，实现了Http请求的缓存。目的不是介绍Block，是为了分享我自己在项目中运用Block的例子，如果读者有更好的方法，欢迎留言啊~~\n参考、推荐阅读 Working with Blocks ","date":"2015-02-19T00:00:00Z","permalink":"/2015/02/19/Block%E7%B1%BB%E5%9E%8B%E5%8F%98%E9%87%8F-%E7%BC%93%E5%AD%98Http%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%9B%9E%E8%B0%83/","title":"Block类型变量-缓存Http请求与回调"},{"content":"前言 随着App的用户界面的内容越来越丰富，再强的手机可能都无法同时渲染复杂的UI界面和保证流畅的体验。所以，我们这些程序猿=。=在写代码的时候就要注意，如何尽可能提高用户的操作流畅性。\n之前的做的项目，青桔音乐iOS客户端里面的首页就是一个类似微信朋友圈的“动态”页面，大致如下：\n如果是你，你会怎么实现这个页面呢？\n这还用问，当然是用UITableView+自定义的UITableViewCell。\nUITableView是可以滑动的，为了不让用户在滑动中感到有卡顿，该如何优化？下面，我就写一下我自己在做项目时的“经验”~\n优化 主要分为以下几点：\n只定义一种Cell。 提前计算并缓存每个Cell的高度。 提前创建真正显示的、需要加工的数据并缓存。 缓存View！ 其它。 只定义一种Cell 乍一看，这个界面至少有3种样式的Cell，为什么只定义一种呢？\n分析结构 仔细分析一下，页面中每个Cell的内容都有头像、标题、正文、评论、其它（歌曲、图片、歌手）。所以，从整体上看，每个Cell的结构是一致的！\n重用=大致固定数量的Cell 并且，凡是认真研究过UITableView的人应该都知道，Apple已经为我们提供了Cell的重用，如用“registerNib:forCellReuseIdentifier”方法注册自定义Cell的Nib，然后在“cellForRowAtIndexPath”的时候用“dequeueReusableCellWithIdentifier”获取可以重用的Cell，所以，无论UITableView要显示内容有多少，真正创建出的Cell可能只有5、6个。\n所以，我们完全可以只创建一种Cell，虽然这样一个Cell的“体积”可能会很大，但是介于Cell的数量不会很多，所以完全可以接受。\n只定义一种Cell的好处 减少代码量，减少Nib文件的数量，统一一个Nib文件定义Cell，容易修改、维护。 基于Cell的重用，真正运行时铺满屏幕所需的Cell数量大致是固定的，设为N个。所以如果如果只有一种Cell，那就是只有N个Cell的实例；但是如果有M种Cell，那么运行时最多可能会是“M x N = MN”个Cell的实例，虽然可能并不会占用太多内存，但是能少点不是更好吗。 善用hidden隐藏（显示）Subview 既然只定义一种Cell，那该如何显示不同类型的内容呢？ 答案就是，把所有不同类型的view都定义好，放在cell里面，通过hidden显示、隐藏，来显示不同类型的内容。如下图定义Cell：\n图中的Subview1、Subview2、Subview3就是不同类型Cell的不同之处，所以我们在“cellForRowAtIndexPath”函数中，设置Cell的样式、内容时，就可以通过显示、隐藏这三个子view来显示。\n毕竟，在用户快速滑动中，只是单纯的显示、隐藏subview比实时创建要快得多。\n提前计算并缓存每个Cell的高度 开发过Android，用过Android的ListView以后，对UITableView需要提前计算Cell的高度很不适应。=。=\n首先要确定的是，在iOS中，系统会先调用“tableView:heightForRowAtIndexPath:”获取每个Cell即将显示的高度，从而确定整个UITableView的布局。然后才调用“tableView:cellForRowAtIndexPath”获取每个Cell，我们也是在这里填充、设置Cell的。\n所以，既然高度总会被用到，那就早早的在获取数据时就计算好吧！\n在Model（Entity）中计算并保存Cell的高度 其实，在Model（Entity）中保存UI的参数是很奇怪的=。=（最好放在ViewModel中，就是MVVM模式的），我们的Entity可能就是下面的样子：\n@interface DataEntity : NSObject //原始数据 @property(copy, nonatomic) NSString *content; @property(copy, nonatomic) NSString *title; //Cell 高度 @property(assign, nonatomic) CGFloat cellHeight; //计算高度 - (void)calculateCellHeight; @end 这样，就不用在“tableView:heightForRowAtIndexPath:”中每次都计算了。\n提前创建真正显示的、需要加工的数据并缓存 Cell中显示的内容，很多时候可能并不是直接从服务器拿到的数据，而是经过“加工”的数据。如本文中的“动态”也，每个Cell的标题、正文都有可点击的连接Link、表情图片等富文本内容，而我们一般用NSAttributeString类来显示。\n既然每次都会用到，倒不如在获取到数据的时候就创建、加工好这些内容，等到需要现实的时候，直接拿来用不就行了。\n所以，我们的Entity类可能变成下面这个样子:\n@interface DataEntity : NSObject //原始数据 @property(copy, nonatomic) NSString *content; @property(copy, nonatomic) NSString *title; //Cell 高度 @property(assign, nonatomic) CGFloat cellHeight; //真正显示的内容 @property(strong, nonatomic) NSAttributedString *showTitle; @property(strong, nonatomic) NSAttributedString *showContent; //计算高度 - (void)calculateCellHeight; //创建、加工真正显示的内容 - (void)setupShowTitileAndContent; @end 这样，在“tableView:cellForRowAtIndexPath”中，我们直接拿showTitle、showContent来显示就好，不用再创建。\n缓存View! 什么？缓存View？！\n是的，当Cell中的部分View是非常独立的，并且不便于重用的，而且“体积”非常小，在内存可控的前提下，我们完全可以将这些view缓存起来！\n方法当然也是将缓存的view放在Entity中~。\n其它 当然，还有其他的优化方法，简单说一说：\n尽量设置Cell的view为opaque，避免GPU对Cell下面的内容也进行绘制。 避免大量的图片缩放、颜色渐变等。 避免同步的从网络、文件获取数据（这个是必须的=。=） 用shadowPath创建阴影。 尽量减少subview的数量，如多用drawRect绘制元素，替代用view显示。 尽量显示“大小刚好合适”的图片资源。 总结 总的来说，就是：\n缓存一切可以缓存的！就是“用空间替换时间”！\n在UITableView的Delegate、DataSource方法中，减少任何不必要的操作\n参考，推荐阅读 25 iOS App Performance Tips \u0026amp; Tricks Tricks for improving iPhone UITableView scrolling performance? How can I speed up a UITableView? ","date":"2015-02-19T00:00:00Z","image":"/assets/2015/02/19/提升UITableView性能-复杂页面的优化/blog_ios_speed_up_uitableview_1_1.png","permalink":"/2015/02/19/%E6%8F%90%E5%8D%87UITableView%E6%80%A7%E8%83%BD-%E5%A4%8D%E6%9D%82%E9%A1%B5%E9%9D%A2%E7%9A%84%E4%BC%98%E5%8C%96/","title":"提升UITableView性能-复杂页面的优化"},{"content":"前言 不管是用什么语言开发，字符串应该算是用的最多的了。在Objective-C中，字符串类是NSString，以及其子类NSMutableString等。所以，对于字符串类，编程语言、编译器等往往会做特殊处理。下面我就说说我自己在项目中发现的NSString的“奇怪”的地方。\n字符串常量，copy，mutableCopy Example 先看看如下面的代码，猜猜会输出什么：\nNSString *str1 = @\u0026#34;abc\u0026#34;; NSString *str2 = @\u0026#34;abc\u0026#34;; NSString *str3 = [[NSString alloc] initWithString:@\u0026#34;abc\u0026#34;]; NSString *str4 = [str1 copy]; NSString *str5 = [str1 mutableCopy]; //输出内存中的地址 NSLog(@\u0026#34;str1: %p\u0026#34;, str1); NSLog(@\u0026#34;str2: %p\u0026#34;, str2); NSLog(@\u0026#34;str3: %p\u0026#34;, str3); NSLog(@\u0026#34;str4: %p\u0026#34;, str4); NSLog(@\u0026#34;str5: %p\u0026#34;, str5); 直接给出结果：\n//地址值由运行时决定，不唯一 str1: 0x10cd12040 str2: 0x10cd12040 str3: 0x10cd12040 str4: 0x10cd12040 str5: 0x7f874b508350 //mutable copy 看，是不是很“神奇”。\n简单解释 NSString是“不可变（immutable）”的类型，这个是关键。 代码中的“@”开头的字符串，其实就是一个字符串常量，运行时会检测这个字符串是否已经存在，存在的话，就直接将这个字符串的地址赋给变量；不存在的话，则创建，再赋值。\n第一行: “abc”字符串不存在，创建，取得字符串地址“0x10cd12040”，赋给str1.\n第二行: “abc”字符串已经存在，直接将地址“0x10cd12040”赋给str2.\n第三行: （记住，NSString是不可变的）alloc并不会分配新的内存，直接将字符串地址“0x10cd12040”赋给str3.\n第四行: copy，既然是不可变的，也就没有必要分配新内存，直接将地址“0x10cd12040”赋给str4.\n第五行: mutableCopy其实就是创建一个NSMutableString的实例，既然是“可变（mutable）”的类型了，当然要分配新的内存，所以，字符串的地址也变成新的“0x7f874b508350”。\n可以这么说，编译器、runtime充分利用了NSString是“不可变（immutable）”类型这个特点，只要字符串内容一致，就不会分配新的内存储存，字符串在程序中使用量非常大，这么做无疑可以大大节省内存，提升性能。\nProperty 和 NSString 先来看看一个UserEntity类：\n//UserEntity.h @interface UserEntity : NSObject \u0026lt;NSCopying\u0026gt; @property(strong, nonatomic) NSString *name; @end 只有一个名为name的NSString类型Property。 相信大家对这个不陌生吧。一般我们的应用中会有很多Entity类型，每个Entity类里面肯定避免不了有许多NSString的字符串类型。这里的name的属性是：strong和nonatomic，strong表明name要“持有”一份name所指的字符串的引用，nonatomic表示property类型不用加锁。\n接着咱们看看下面的代码：\nUserEntity *userEntity = [UserEntity new]; //创建mutable类型的字符串 NSMutableString *showName = [[NSMutableString alloc] initWithString:@\u0026#34;tutuge\u0026#34;]; //先保存“tutuge”字符串到userEntity的name userEntity.name = showName; //修改showName [showName appendString:@\u0026#34; blog\u0026#34;]; //输出userEntity的name属性 NSLog(@\u0026#34;Name: %@\u0026#34;, userEntity.name); 猜猜会输出什么？\nName: tutuge blog 看，我们明明是保存的“tutuge”，怎么到后面输出的是“tutuge blog”？\n让NSString类型的Property为Copy型 因为name的Property属性是strong的，所以赋值后，它跟showName均指向同一个NSMutableString实例，如下：\n所以改变了showName，当然name输出的值也就变了。 这个时候，name的property属性就不能是strong了，应该是copy，如下：\n//UserEntity.h @interface UserEntity : NSObject \u0026lt;NSCopying\u0026gt; @property(copy, nonatomic) NSString *name; @end 这样的话，当“userEntity.name = showName;”这句运行后，内存布局如下：\n这样，就避免了这个问题。\n总结 很所时候，代码并不是看上去那样“运行”的，多试试，多查阅资料，就能了解其中的奥秘了~\n","date":"2015-02-18T00:00:00Z","image":"/assets/2015/02/18/NSString的Copy与内存分配/blog_ios_nsstring_copy_1_1.jpg","permalink":"/2015/02/18/NSString%E7%9A%84Copy%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D/","title":"NSString的Copy与内存分配"},{"content":"更新 2015-12-21: 其实用Category按照业务拆分网络请求接口的代码也是非常棒的，目前我自己就是这么做的=。=，文章就当做熟悉下NSProxy吧~ 前言 之前在做项目的时候，所有业务的网络接口方法，全部都写在了一个文件里面，一开始还好，毕竟每个方法的代码也只是十几行，增加、修改也比较容易。但是随着接口的增多，这个文件慢慢居然超过了1000行，里面几十个方法都写在一起，实在是不好维护。\n虽然保持这样也没有什么，多用用Cmd+F就能找到。但是，真是越看越不顺眼😳， 代码一定要有“美感”啊~😝\n所以在查阅了大量文章后，终于想出了本文即将说明的办法，越发觉得Objective-C的“消息”机制是多么的赞~😚\nGithub 示例 贴上本文中的示例工程：https://github.com/zekunyan/HttpProxyExample\n问题 先抛出问题。\n一款互联网应用，免不了要跟服务器打交道，在iOS项目中，最有名的网络库应该就是AFNetworking了。所以，很多人就会利用AFnetworking提供的Get、Post等基本Http请求接口，封装自己的网络接口层代码，我自己在项目中也是这么做的。\n但是，AFNetworking只是提供了Get、Post、Json传输等基本的Http请求方法，所以一旦落实到具体的业务相关的请求上，我们要为每个请求（URL）都写一个单独的接口方法。\n那么，问题就来了\n业务相关的接口那么多，举个例子，什么“通过用户ID获取用户基本信息”、“获取用户的所有评论”等，每个请求都是一个方法，这么多方法该怎么组织呢？全部放在一起？那这个接口类岂不是会非常乱？不放在一起？那岂不是会有很多个网络请求类？(至于要不要统一接口入口，我想这个根据项目来决定吧=。=)\n需求 所有网络接口都从统一的类调用，如HttpProxy。 网络接口的具体实现，按照业务划分到不同的类中，如“UserHttpHandler”、“CommentHttpHandler”。 其实，按照面向对象的原则，就是接口代理类HttpProxy拥有若干个按照业务划分的接口（Interface），这些接口的所有方法组成了网络层的不同的Http请求。如下图：\n那么，调用的时候，所有接口都用HttpProxy调用，如：\n//实际调用的是UserHttpHandler类的方法 [[HttpProxy sharedInstance] getUserWithID:@100]; //实际调用的是CommentHttpHandler类的方法 [[HttpProxy sharedInstance] getCommentsWithDate:date]; 关键 根据前面的描述，我们可以得出，关键就是：消息转发（Message Forward）\nObjective-C里面没有我们传统的“方法调用”，取而代之的是“消息”，所有的方法都是通过向对象发送“消息”实现调用的。而这个机制，也就为我们的实现提供了方便。\n也就是说：我们要将发给“HttpProxy”的消息，让HttpProxy转发给真正能接受这个消息的对象，HttpProxy就是个代理。\n苹果已经给我们提供了这个“代理”类了-NSProxy。\nNSProxy 什么是NSProxy：\nNSProxy没有父类，是顶级类(根类)，跟NSObject同等地位。 NSProxy和NSObject都实现了“NSObject Protocol”。 NSProxy设计时就是以“抽象类”设计的，专门为转发消息而生。 实现要求:\n继承NSProxy的子类要实现自己的初始化方法，如“init”、“initWith”。 重写- forwardInvocation:和- methodSignatureForSelector:方法，完成消息转发。 详细内容参考Apple的文档。\n实现 定义 先不管HttpProxy，咱们看看具体的接口，先举两个例子：\n//UserHttpHandler.h //用户相关接口 @protocol UserHttpHandler \u0026lt;NSObject\u0026gt; - (void)getUserWithID:(NSNumber *)userID; @end //CommentHttpHandler.h //评论相关接口 @protocol CommentHttpHandler \u0026lt;NSObject\u0026gt; - (void)getCommentsWithDate:(NSDate *)date; @end 好的，接口有了，我们的HttpProxy类应该“实现”了这两个接口。 然后，最好是单例类，所以还要有个获取单例的方法。 最后，还需要一个向HttpProxy注册具体实现了接口Protocol的方法。\n所以，HttpProxy应该是这个样子的：\n//HttpProxy.h //1. 继承了NSproxy。 2. “实现”了网络接口Protocol @interface HttpProxy : NSProxy \u0026lt;UserHttpHandler, CommentHttpHandler\u0026gt; //获取单例 + (instancetype)sharedInstance; //注册具体实现类 - (void)registerHttpProtocol:(Protocol *)httpProtocol handler:(id)handler; @end 找到消息对应的实现类对象 如何在HttpProxy做消息转发时，找到某个消息对应的真正的实现类对象呢？\n最好的办法就是保存每个接口方法到其实现类对象的映射，可以用Dictionary保存，关系如下图：\n所以，registerHttpProtocol:handler:方法的职责就是：\n遍历Protocol的所有方法（利用Objective-C的Runtime）。 保存Protocol所有方法到实现类的对象的映射关系。（用方法的字符串表示作为key，实现类对象为value） 所以，HttpProxy应该持有一个Dictionary的实例，用于保存映射关系，HttpProxy的实现部分如：\n//HttpProxy.m @interface HttpProxy () //保存映射关系的字典。 @property(strong, nonatomic) NSMutableDictionary *selToHandlerMap; @end 注册方法实现如下：\n- (void)registerHttpProtocol:(Protocol *)httpProtocol handler:(id)handler { unsigned int numberOfMethods = 0; //获取Protocol的所有方法 struct objc_method_description *methods = protocol_copyMethodDescriptionList( httpProtocol, YES, YES, \u0026amp;numberOfMethods); //为Protocol的每个方法注册真正的实现类对象handler for (unsigned int i = 0; i \u0026lt; numberOfMethods; i++) { struct objc_method_description method = methods[i]; [_selToHandlerMap setValue:handler forKey:NSStringFromSelector(method.name)]; } } 实现消息的转发 我们已经可以注册接口、保存映射关系了，剩下的就是重写NSProxy的两个方法，以实现消息的转发，至于这两个方法具体作用是什么，读者可以自行查阅相关资料。如下：\n//HttpProxy.m //获取Method signature - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { //获取method的字符串表示 NSString *methodsName = NSStringFromSelector(sel); //查找对应实现类对象 id handler = [_selToHandlerMap valueForKey:methodsName]; //再次检查handler是否可以相应此消息 if (handler != nil \u0026amp;\u0026amp; [handler respondsToSelector:sel]) { return [handler methodSignatureForSelector:sel]; } else { return [super methodSignatureForSelector:sel]; } } //转发方法消息 - (void)forwardInvocation:(NSInvocation *)invocation { NSString *methodsName = NSStringFromSelector(invocation.selector); id handler = [_selToHandlerMap valueForKey:methodsName]; if (handler != nil \u0026amp;\u0026amp; [handler respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:handler]; } else { [super forwardInvocation:invocation]; } } Example 看看如何使用HttpProxy：\n//初始化，注册Protocol对应的实现类对象 [[HttpProxy sharedInstance] registerHttpProtocol:@protocol(UserHttpHandler) handler:[UserHttpHandlerImp new]]; [[HttpProxy sharedInstance] registerHttpProtocol:@protocol(CommentHttpHandler) handler:[CommentHttpHandlerImp new]]; //调用 [[HttpProxy sharedInstance] getUserWithID:@100]; [[HttpProxy sharedInstance] getCommentsWithDate:[NSDate new]]; 总结 所有的代码及示例都提交到Github了，HttpProxyExample。\n总的来说，就是利用Objective-C的“消息”机制，继承NSProxy抽象类，实现自己定义的转发机制，将网络接口层的各个方法的实现与声明分离，提升项目代码的可维护性，更加模块化。如下图表示:\n以上，就是我自己在项目中，利用NSProxy设计并实现的网络接口层结构。💪\n从了解NSProxy、Runtime到设计、实现出这个网络层架构，着实花了点时间，收获真是不少。其实像这种纯代码层次的重构，很多人会觉得“吃力不讨好”😨，但我觉得，正是这样，才能真正让自己提升。\n代码是具有美感的！嗯！😝\n参考 NSProxy Class Reference NSProxy, NSObject’s Lesser-Known Sibling Objective-C Runtime Programming Guide ","date":"2015-02-16T00:00:00Z","image":"/assets/2015/02/16/利用NSProxy实现消息转发-模块化的网络接口层设计-原创/blog_ios_http_proxy_1_1.jpg","permalink":"/2015/02/16/%E5%88%A9%E7%94%A8NSProxy%E5%AE%9E%E7%8E%B0%E6%B6%88%E6%81%AF%E8%BD%AC%E5%8F%91-%E6%A8%A1%E5%9D%97%E5%8C%96%E7%9A%84%E7%BD%91%E7%BB%9C%E6%8E%A5%E5%8F%A3%E5%B1%82%E8%AE%BE%E8%AE%A1-%E5%8E%9F%E5%88%9B/","title":"利用NSProxy实现消息转发-模块化的网络接口层设计-原创"},{"content":"前言 item 3讲的是Objective-C里面的“字面量语法”-“Literal Syntax”，个人觉得非常有用。\nItem 3 - Prefer Literal Syntax over the Equivalent Methods 本节讲了Objective-C中的“字面量语法”，并且建议我们更多的使用这种语法，而不是使用传统的等价的函数方法。\n引子 当我们使用Objective-C编程的时候，总会经常使用到几种Foundation框架中的基本类，如NSString、NSNumber、NSArray、NSDictionary，或者是它们的子类。例如，当我们想创建一个字符串的时候，会这么写：\nNSString *str = @\u0026#34;a string\u0026#34;; 不知道各位写这句的时候有没有觉得奇怪？为什么不用先“alloc”，再“initWithXXX”这么来创建呢。是的，这里创建字符串，就用到了Objective-C提供的“字面量语法”-“Literal Syntax”，而这种语法的“标志”就是“@”符号。 有了字面量语法，我们就不用每次创建一些基本类型的时候，写一大堆alloc、init了。\nNSNumber 字符串可以用字面量语法创建，数字类NSNumber肯定也可以，如下：\nNSNumber *intNum = @1; NSNumber *floatNum = @1.1f; NSNumber *doubleNum = @1.1; NSNumber *boolNum = @YES; NSNumber *charNum = @\u0026#39;a\u0026#39;; //不用字面量语法 NSNumber *intNumber = [[NSNumber alloc] initWithInt:1]; 看，是不是很方便~\nNSArray 创建 数组算是最常用的一种数据结构了，如果没有字面量语法，创建数组可能是这样：\nNSArray *numbers = [[NSArray alloc] initWithObjects:@1, @1.1, @2.0f, @YES, nil]; 用字面量语法的话，就是这样：\nNSArray *numbers = @[@1, @1.1, @2.0f, @YES]; 存取值 创建了数组，肯定要从数组里面取值，用NSArray的方法就是这样：\n//取 NSNumber *aNumber = [numbers objectAtIndex:2]; //存 NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithArray:@[@1, @2, @3]]; [mutableArray replaceObjectAtIndex:1 withObject:@4]; 跟C语言、Java这些语法很不一样啊，人家都是通过中括号“[]”直接用下标取值就可以。有了字面量同语法，Objective-C也可以这样:\n//取 NSNumber *aNumber = numbers[2]; //存 NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithArray:@[@1, @2, @3]]; mutableArray[1] = @4; 要注意的地方 传统的通过“initWithObjects”方法创建数组的时候，是根据最后的“nil”判断数组是否结束的，也就是说，用于初始化数组的元素中有“nil”值也不会导致异常（Exception），如下语句：\nNSNumber *number1 = @1; NSNumber *number2 = nil; //空指针 NSNumber *number3 = @3; //创建 NSArray *numbers = [[NSArray alloc] initWithObjects: number1, number2, number3, nil]; //输出为1，因为number2，创建到number2就截止了 NSLog(@\u0026#34;numbers count: %d\u0026#34;, numbers.count); 但是，如果用字面量语法创建，就是出问题：\nNSNumber *number1 = @1; NSNumber *number2 = nil; NSNumber *number3 = @3; //创建出现异常！！！ NSArray *numbers = @[number1, number2, number3]; NSLog(@\u0026#34;numbers count: %d\u0026#34;, numbers.count); 因为字面量语法只是单纯的将中括号中的元素全部加到数组中，但是，Objective-C中的数组是不可以存放nil值的，所以程序会运行时抛出异常。\nNSDictionary 创建 如果不用字面量语法创建字典，传统的创建函数简直令人匪夷所思：\n// number1-\u0026gt;@1 // number2-\u0026gt;@2 NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:@1, @\u0026#34;number1\u0026#34;, @2, @\u0026#34;number2\u0026#34;, nil]; 是不是很奇怪，value在前面，key在后面\n所以，还是用新的语法吧：\nNSDictionary *dict = @{@\u0026#34;number1\u0026#34; : @1, @\u0026#34;number2\u0026#34; : @2}; 这样才符合“常识”嘛，key在前，value在后面，跟JSON的定义一样。\n存取值 取值的语法跟NSArray的语法很想，只不过是将下标数字变成了key：\n//传统 NSNumber *number1 = [dict objectForKey:@\u0026#34;number1\u0026#34;]; [dict setValue:@5 forKey:@\u0026#34;number1\u0026#34;]; //字面量语法 NSNumber *number2 = dict[@\u0026#34;number1\u0026#34;]; dict[@\u0026#34;number1\u0026#34;] = @6; 注意 需要注意的地方，跟创建数组一样，NSDictionary的value不能为nil值，否则会抛出异常。\n总结 总的来说，使用字面量语法可以让代码更加清晰、简单，毕竟，更少的代码，有时候意味着更少的错误。\n","date":"2015-02-14T00:00:00Z","permalink":"/2015/02/14/Effective-Objective-C-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-Item-3/","title":"Effective-Objective-C-读书笔记-Item-3"},{"content":"前言 第二弹来了。\nItem 2 - Minimize Importing Headers in Headers 这一节讲的是尽量在一个头文件中减少其它头文件的引入。\n头文件 Objective-C和C语言、C++很像，都将类的实现与声明分开，用.h后缀名文件表示声明文件，用.m文件实现类。当要用到这个类的时候，只需要引入头文件即可，至于编译器、运行时是如何知道头文件里面的类对应的实现在哪里，在这了不做说明。一下用例子说明，先看看下面的类：\n//Wheel.h #import \u0026lt;Foundation/Foundation.h\u0026gt; //车轮类 @interface Wheel : NSObject @property(copy, nonatomic) NSString *manufacturer; @property(assign, nonatomic) NSUInteger radius; @end 第二个类\n//Engine.h #import \u0026lt;Foundation/Foundation.h\u0026gt; //发动机类 @interface Engine : NSObject @property(copy, nonatomic) NSString *manufacturer; @property(assign, nonatomic) NSUInteger power; @end 然后就是车类Car：\n//Car.h #import \u0026lt;Foundation/Foundation.h\u0026gt; //汽车类，只有一个轮子，不要奇怪=。= @interface Car : NSObject @property(strong, nonatomic) Wheel *wheel; @property(strong, nonatomic) Engine *engine; @end 注意到少了什么东西没有？ 是的，写过C语言、C++的童鞋一下就能发现，Car类没有引入Engine和Wheel的头文件，肯定会编译出错的，因为编译器不知道Engine、Wheel类是啥。 那好，我们加上下面两行：\n#import \u0026#34;Wheel.h\u0026#34; #import \u0026#34;Engine.h\u0026#34; 嗯。这样就不会出错了。但是这样真的好吗？Objective-C给我们提供了@class关键字，就是来解决这个问题的。\n前置声明（forward declaration） 何为前置声明？看看下面的Car类的头文件例子。\n//Car.h #import \u0026lt;Foundation/Foundation.h\u0026gt; //前置声明 @class Wheel; @class Engine; @interface Car : NSObject @property(strong, nonatomic) Wheel *wheel; @property(strong, nonatomic) Engine *engine; @end @class就是类的前置声明（forward declaration），就是告诉编译器“嗨，不用找了，Wheel类和Engine类是肯定存在的，用到的时候再找”。有了前置声明，我们就不用显式的引入Wheel类和Engine类了。\n当然，在Car类的实现文件.m文件中，我们还是要显式的引入Wheel和Engine类的，因为在这里我们要具体用到这两个类了，当然要知道类的细节。\n为何不要import？ 为什么不直接import呢？因为如果直接用import引入Wheel和Engine的声明，那么任何import引入了Car类的文件，也同时会引入Wheel、Engine类的声明，而且最终我们可能并不一定会直接跟Wheel、Engine类打交道，这样不就引入了“没有用”的类了吗？而且这样做很可能造成文件引入成“环”。\n虽然import可以避免重复声明造成编译出错，传统的C语言、C++在声明的时候也可以通过如下方式避免重复声明：\n#ifndef _WHEEL_H_ #define _WHEEL_H_ //声明内容... #endif 但是既然Objective-C有@class这种前置声明的办法，为何不用呢。\n必须用import的时候 当然，@class这样的前置声明并不能解决一切头文件引入的问题，如下这样的类，就必须要用import：\n#import \u0026lt;Foundation/Foundation.h\u0026gt; //Car 的声明 #import \u0026#34;Car.h\u0026#34; //Driver protocol的定义 #import \u0026#34;Driver.h\u0026#34; @interface BigCar : Car \u0026lt;Driver\u0026gt; @end 是的，当类需要被继承、定义的protocol需要实现的时候，就需要import相关的头文件了（protocol的实现如果跟类的使用者没有关联，可以定义在类的实现文件中的“扩展category”中）,当然，还有@protocol这样的protocol的前置声明，怎么用就留给读者自己查阅相关资料了。\n总结 大费周章的讲了这么多，其实目的就是一个：尽量少在头文件里面引入其他头文件。\n最终的目的就是只暴露最少的细节。\n写代码有段时间了，一直都在琢磨这句话，希望读者也能好好体会~\n","date":"2015-02-06T00:00:00Z","permalink":"/2015/02/06/Effective-Objective-C-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-Item-2/","title":"Effective-Objective-C-读书笔记-Item-2"},{"content":"前言 一直都在做项目，没什么时间作总结，感觉很不是滋味。现在放假了，嗯，终于有时间了~~哈哈（天生程序猿一枚好不好=。=） 最近开始读英文版的Effective Objective-C 2.0，觉得不错，就总结些东西，分享给各位读者吧。\nItem 1 - Familiarize Yourself with Objective-C’s Roots 第一章的目的，就是让大家熟悉Objective-C，自然，标题的意思也就是“了解Objective-C的基本概念”，不多说，开写。\n消息与运行时 总的来说，Objective-C跟C++、Java这些面向对象的语言是非常相似的，但是还是有许多不同的地方。如果你是第一次看到Objective-C的语法，一定觉得非常奇怪，因为Objective-C使用的是基于消息（Message）的框架，而不是传统的方法、函数调用。如下：\n//Objective-C Person *person = [Person new]; [person setName:@\u0026#34;tutuge\u0026#34; age:24]; //Java Person person = new Person(); person.setNameAndAge(\u0026#34;tutuge\u0026#34;, 24); //C++ Person *person = new Person; person-\u0026gt;setNameAndAge(\u0026#34;tutuge\u0026#34;, 24); 很奇怪是不是，各种中括号。但是我觉得，Objective—C这么设计就是为了时刻提醒你，“我是基于消息机制的！”。消息，的确是Objective-C与Java、C++这种语言最大的不同，而且，这种不同远不止在语法上面。简单来说如下两点：\n基于消息的调用： 所有的函数调用都是在“运行时”决定的。 基于方法的调用： 所有的函数调用都是在“编译时”决定的，就是编译器编译时决定的。 其实，连被调方法的对象类型，都是在运行时决定的。所以说，Objective-C的运行时系统，担负了绝大部分的工作。这么设计的优点，举个例子，就是当运行时系统升级时，原有的代码不用重新编译即可“享受”升级带来的好处。\nC的超集 Objective-C是C语言的超集，所以C语言所具有的一切特性，Objective-C都可以兼容，充分理解C语言的内存模型，对理解Objective-C很有帮助。如下：\nNSString *str = @\u0026#34;a string\u0026#34;; 这一行代码跟C语言没有什么不同，（忽略@符号=。=），原理跟C语言也是一致的，就是str指针是NSString类型的指针，指向了一块存着字符串“a string”的内存区域。不同的是，在Objective-C里，所有的对象只可以声明为指针类型，所以如下代码是不合理的：\nNSString str; 为了更充分的理解，咱们看看如下例子：\nNSString *str1 = @\u0026#34;a string\u0026#34;; NSString *str2 = str1; 熟悉C语言的朋友应该知道这是怎么回事，内存当中只有一块区域储存着“a string”这个字符串，str1、str2都是指向着这个区域的字符串指针。如下图：\nstr1、str2都在栈内存上分配的，就是字符串（NSString）类型的指针。 “a string”字符串实际储存在堆内存上。 str1、str2均指向同一块内存区域。 此处的栈内存、堆内存跟C语言中的概念也是一样的，不同的是，在Objective-C中，我们不用手动去malloc、free内存，Objective-C使用内存计数计数帮我们管理内存。\n结构体，基本类型 在Objective-C中，并不是所有的变量都是指针类型的，像int、float、double这些依旧是传统的“栈上分配”的，当然，还有结构体struct、枚举enum、联合union等。如：\nCGPoint point = CGPointMake(10, 10); CGRect frame = CGRectMake(0, 0, 10, 10); 这的CGPoint、CGRect都是结构体，但是都是直接在栈上分配内存的，以减少过度使用Objective-C类型的类（减少运行时的压力），各位读者使用的时候要注意。\n总结 零散的说了一些Objective-C的特点，远远不足以讲明白Objective-C，我这只是根据Effective Objective-C这本书给个引子，还是推荐Apple的官方教程、文档，要想掌握扎实，还是要一步一步来~\n","date":"2015-02-05T00:00:00Z","image":"/assets/2015/02/05/Effective-Objective-C-读书笔记-Item-1/blog_effective_objective_c_item_1_1.jpg","permalink":"/2015/02/05/Effective-Objective-C-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-Item-1/","title":"Effective-Objective-C-读书笔记-Item-1"},{"content":"前言 这段时间一直在做iOS的项目（青桔音乐App），随着代码量的上升，逐渐意识到项目的整体目录结构划分是多么重要，清晰的目录结构，能够更好的应对新的需求，更容易重构。虽然一直都是一个人在做=。=，但是还是深深地体会到目录结构的重要性。\n例子 说再多都不如来个例子实在，如下：(VC指的是viewController)\n|---General |---AppDelegate |---Application |---Constant |---UI |---View |---VC ... |---Category ... |---Entity |---BaseEntity |---UserEntity |---CommentEntity ... |---Http |---Api |---Manager |---CodeHandler |---ErrorHandler |---HttpHandler |---UserHttpHandler |---CommentHttpHandler ... |---DB |---DBHelper |---DBService ... |---Util |---StringUtil |---NumberUtil |---TimeUtil ... |---Service |---AccountService |---ShareService ... |---Lib |---Umeng |---QQSDK |---SVProgressHUD ... |---Module |---Login |---View |---VC |---XIB |---Comment |---View |---VC |---XIB |---Business |---Feeds ... 乍一看好多，别被吓到=。=，容我细细讲解。\n详细讲解 General General好理解，放的就是如AppDelegate之类的、项目中最普通的、最常用的组件。\nAppDelegate:\nApp的Delegate类实现，这个必须有，就不用说了吧。\nApplication:\n如果自定义实现了Application类，就放在这里。\nConstant:\n顾名思义，常量，项目用到的所有公共常量，如enum枚举类型的、Notification的Tag，常用的颜色、字符串等等等，都可以按照自己的需求划分不同的Group，放到Constant里面。\nUI:\n项目中自定义的UIView、UIViewController子类之类的代码，就对应放在UI下的View、VC中。\nCategory:\n对系统类做的扩展，各种Category，如NSString+XXXExtension这种，就统一放在Category目录下。\n如还有其他的通用组件，也不妨放到这里，做统一的管理。\nEntity Entity，也有人喜欢叫Model（关于Entity和Model的区别，推荐看看这篇文章，个人觉得研究研究Entity和Model概念的区别还是很有好处的），就是程序中的“实体”，如一个用户、一条评论、一首歌等等，简单来说就是一个独立“个体”的集合、打包，具体的自己查查吧，网上一大堆的，感觉跟JavaBean的概念比较像。\n通常来说，每个Entity类都比较简单，只包含若干个属性。但是有时候可能要做统一的处理，如，在从服务器取回的JSON数据解包成具体的Entity类，并且执行一系列初始化操作等等，所以可能要对所有的Entity类做统一处理。所以说，可以定制相应的BaseEntity基类，利用模板方法等办法，定制统一的初始化流程（好像扯远了=。=，这个后面会详细写篇东西分享给大家），BaseEntity就是放这些基类的东西的。\n有了BaseEntity后，对应的各个业务中的具体Entity，如UserEntity、CommentEntity，就可以分门别类的建立单独的Group存放。\nHttp 这个Group里面放跟网络请求相关的东西，详细如下：\nApi:\n互联网应用少不了跟服务器打交道的各种网络接口，所以我在项目中把所有的API对应的相对URL地址、参数注释要求等等都放在了这里，好统一管理。\nManager:\n定义最基本的网络请求，如GET、POST、PUT等请求的基本封装，获取图片的基本封装。 一般来说就是定制统一的基本请求接口，对上层提供一致、稳定的服务，真正的网络请求，可以自己用iOS原生的Api写，也可以用AFNetworking等第三方库做封装，图片也可以灵活的用AFNetworking、SDWebImage这些优秀的库实现。还有就是，可以方便的统一对请求做处理，如错误处理、Http的Code、状态处理等等。还可以统一的增加请求参数，如统一为每个请求都增加用户的ID、token什么的。\nCodeHandler\n大部分的API设计都会有相应的状态码、Code，为了方便扩展，可以把这些处理Code的类单独放在这里。\nErrorHandler\n这个就少不了了，对Http的错误进行单独处理，加Log什么的。\nHttpHandler\n这个Group里面放的是真正实现接口的类，如什么UserHttpHandler、CommentHttpHandler之类的，就是具体实现了接口调用、处理返回数据、回调的类。\nHttp里面的各个Group的类其实都是相互关联的，设计的时候可以定制统一的接口（Protocol），然后创建类实现（conform）这些接口，也就是面向接口的编程，以最大限度的减少接口层的各个职能之间的耦合，方便扩展。\nDB 介于iOS的SQLite不是那么好用，所以非常有必要为操作数据库的类建立单独的地盘=。=\nDBHelper:\n放基础的操作数据库的类，如简单的查找、插入、更新、事务更新等等操作，为负载的数据库业务逻辑封装底层接口。比如对流行的FMDB进行封装等等。\nDBService:\n这里放具体的数据库业务实现类，至于为什么叫“Service”，因为我也想不出什么好的名字了=。= 按照自己的业务逻辑组织即可。\nUtil 放常用的工具类的地方。如字符串操作的类StringUtil、时间计算格式化类TimeUtil等，按具体需求而定，这个就不用多说了吧。\nService 项目的需求多了，业务逻辑的代码就会越来越多，总不能都放在view controller里面吧。一些多处用到的，或者非常独立的业务代码，完全可以抽离出来，实现为单独的、跟界面无关的业务类。因为做的事很杂，所以干脆就叫Service了。\nAccountService、ShareService就是这种类。而且大部分的Service都应该是单例类，如AccountService类可能维护着程序运行期间的账户信息，ShareService对程序的分享功能做了统一处理等等，具体怎么用就随各位了。\nLib Lib，放各种第三方库，因项目需要修改过的第三方组件等，像什么友盟、QQSDK之类的就可以放这。当然，一些不会做改动的库最好还是用CocoaoPod做统一管理。\nModule 终于讲到了最重要的地方。\niOS工程中最多的文件往往就是各种View、ViewController类，以前总是看到有人只创建两个Group，一个叫Views，另一个叫ViewControllers，然后所有的Views、ViewController都往里面塞，然后随着需求的增加，这两个Group也臃肿不堪。。。\n办法总是有的。就是为工程划分模块-Module\n如何划分Module？我认为，可以按照以下两点建立：\n以页面跳转分支划分。 以功能划分。 以页面跳转分支划分\n就是按照应用的页面设计与业务逻辑，从最顶级开始，一级一级页面往下跳转，找出其中的独立分支，归为一个Module模块。 举例来说，应用主界面有4个Tab页，就先分出四个Module，然后一级一级往下跳转，遇到分支就建立新的Module，如此递归的建立，就能大致划分出各个Module。当然，这么做是最粗糙的，还要根据情况，将不同的分支Module合并成一个Module，简化代码的组成。我在这只是提供个划分Module的方法，具体怎么设计就看各位读者了=。=\n以功能划分\n这个好理解，无非就是根据前期项目的功能模块划分工程的代码Module组成。如什么用户设置Module、评论Module、登录Module等等。\n总的来数，就是要用Module将工程的代码分类管理，每个Module具有大致相同的结构，如都可能有本Module用到的View、ViewController，自定义的类Class等等，就是说，按照职能对代码划分，避免将所有的类都堆在一起，也好应对新的需求。\n总结 啰啰嗦嗦说了一大堆，想必各位都看烦了吧=。=刚开始写博客，写到一个地方，又会引出另一块，写着写着容易写偏了。\n写之前我也大致搜了一下，发现网上相关的资料好少，但是实际的经验告诉我，项目工程的目录结构非常重要，无论是对开发还是对后期的维护，所以想着应该写篇东西，分享我自己的经验给大家，有啥问题，多提意见啊~~~\n","date":"2015-02-01T00:00:00Z","permalink":"/2015/02/01/iOS%E9%A1%B9%E7%9B%AE%E7%9A%84%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84-%E5%8E%9F%E5%88%9B/","title":"iOS项目的目录结构-原创"},{"content":"前言 近来因为项目的缘故，仔细研究了一下如何在Android的TextView里面绑定可以点击的链接，并且定制链接的颜色，包括正常状态、按下去时的文字、背景的颜色，当然，还有点击后的回调。\n为了以后方便自己，也为了方便更多的人，就把这些封装成了一个简单的开源库，也算是我自己的第一个开源库吧。\nLinkTextView 名称：LinkTextView。喜欢的话就Star一下吧。\nExample 功能： 可以方便的对TextView里面的文字增加可点击链接，并且绑定回调。 可以为每个链接单独设置文字、链接背景的正常、按下状态下的颜色。 可以为每个链接绑定任意类型的“附件”对象，作为点击链接时的回调参数。 讲解 下面分重点讲解一下。\n1. 为文字增加可点击链接，绑定回调。 为文字增加链接，已经是老生常谈了，真是一搜一大把，不过在这里还是简单讲一下。\n用过TextView应该都知道这个接口：\npublic final void setText (CharSequence text) 其中CharSequence接口的子接口里面有Spannable，而由这个接口派生出来的类SpannableString，就是我们可以对TextView里面的文字做附加效果的原因。如改变颜色、增加超链接等，有兴趣的各位可以去仔细查查。下面简单的说说怎么加可点击的链接。\n首先创建SpannableString：\nSpannableString text = new SpannableString(\u0026#34;LinkTextView dadada\u0026#34;); 绑定ClickableSpan回调\nClickableSpan clickableSpan = new ClickableSpan() { @Override public void onClick(View view) { //点击了链接，做你想干的事吧=。= } }; text.setSpan(clickableSpan, 0, 5, 0); 最后再setText\ntextView.setText(text); 看，绑定可以点击的链接就是这么简单。\n2. 改变正常、按下时的文字、链接背景颜色。 往往系统默认的链接颜色是不能满足我们的需求的，所以最好能对链接的文字、背景的按下、松开时的颜色做定制。\n但是好坑=。=，Google居然没有为ClickableSpan增加按下时的回调\n那就只好自己造轮子了。\n首先 必须要能获取到链接按下时的回调，应该是onTouch什么的，于是，就查到了类：LinkMovementMethod。 在TextView中可以用方法：public final void setMovementMethod (MovementMethod movement)设定监听，而LinkMovementMethod类里面就可以监听到链接的onTouch！\nLinkMovementMethod的onTouch就是介个样子的\nboolean\tonTouchEvent(TextView textView, Spannable spannable, MotionEvent event) 于是，我们可以通过event来判断是按下还是移动等等操作。 但是问题是，这里的spannable是整个TextView全部的文字，所以，只能通过event里面的x、y坐标，找到具体点击的是哪个链接。这里，要感谢stack overflow上的大神的帖子。\n具体的找点击链接的方法如下：\nprivate TouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) { //获取点击的x、y坐标 int x = (int) event.getX(); int y = (int) event.getY(); //减去Padding x -= textView.getTotalPaddingLeft(); y -= textView.getTotalPaddingTop(); //加上滚动的偏移 x += textView.getScrollX(); y += textView.getScrollY(); //获取第几行、以及偏移 Layout layout = textView.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); //获取真正点击的链接 TouchableSpan[] link = spannable.getSpans(off, off, TouchableSpan.class); TouchableSpan touchedSpan = null; if (link.length \u0026gt; 0) { touchedSpan = link[0]; } return touchedSpan; } 到此，我们已经能获取到链接的点击状态，并且捕捉到具体点击的ClickableSpan。\n接着 就是改变链接的文字、背景颜色。\nClickableSpan的方法里面，除了\nvoid onClick(View widget); 还有就是：\npublic void updateDrawState(TextPaint textPaint); 很明显，就是更新文字渲染效果的。我们用如下两个方法，就能实现：\npublic void setColor (int color); bgColor属性 最后 到目前为止，我们已经可以捕捉到链接的点击状态，能获取到真正的点击的链接，还有能刷新链接的颜色了，相信聪明的你一定能自己琢磨出来，怎么样实现文章开头的效果。=v=\n本文参考 Android官方文档。 StackOverFlow的：Change the text color of a ClickableSpan when pressed。 ","date":"2014-11-02T00:00:00Z","image":"/assets/2014/11/02/LinkTextView-Android开源库-原创/LinkTextView_Sample.gif","permalink":"/2014/11/02/LinkTextView-Android%E5%BC%80%E6%BA%90%E5%BA%93-%E5%8E%9F%E5%88%9B/","title":"Android开源库-LinkTextView-原创"},{"content":"拖了很久，终于开始写博客了。 一直觉得自己的知识储备不够，一直都在学习，一直都在积累。（其实就是懒好吗=。=）\n说点啥呢，想到啥就说啥吧~\n从小就喜欢模型、乐高积木，喜欢拆东西，印象最深的就是把家里的高级录音机拆了，装不回去，塞到了床底下。。。\n上了学之后，又开始捣鼓四驱车，自己做马达，绕线圈，家里零件一大堆，串联一堆电池，组装遥控车，唯一的遗憾，就是没有做成遥控飞机（没钱=、=）。\n接着，家里有了电脑！！！这要感谢老爹，当时花了一万多，液晶显示器，刚上市的奔腾4啊！\n于是，我又开始拆电脑。。。换显卡、超频，跑分，再超频，再跑分，3DMark就是俺的祖宗啊~\n终于，走上了编程这条“不归路”，义无反顾，一直到了现在。\n用Dev-C++折腾C语言，数据结构、算法，接着又是C++，Window API，QT（这个真觉得不错），MFC（软件课设），Java，Python，Android（上线了几个小应用，拿了Google全国大学生Android应用开发大赛华中地区三等奖），iOS（上线应用，青桔音乐），Linux，还有各种MySQL、MongoDB等数据库的纠结，看各种博客。。。\n每天看的很多，想的也不少，但是终究不够精通，路还很长啊~\n当然，最感谢的就是我的女朋友陈因素\n","date":"2014-11-01T00:00:00Z","permalink":"/2014/11/01/%E7%AC%AC%E4%B8%80%E7%AF%87Blog/","title":"第一篇Blog"}]