组件化通信图

设计之初

在编程中,两个变量如何交换?常用的解决方案会用第三个temp临时变量来存储做置换。那组件之间的通信也如此,如果要降低组件和组件之间的耦合或业务与组件之间的耦合,那么中间件是一个值得考虑的方案。你可以理解为它是第三方,或代理,或装饰器,或传递者。市场上常见的组件间通信方案有三种:URL路由、target action. interface or protocol. 各自有相关的优缺点。interface是从后端java衍生过来的,很多语言已经自带了面向接口编程。而swift正是为此诞生,面向协议编程。早在java ssh框架中,我们就看到过Spring ioc,aop等方式,这也就使得后端代码几乎都是面向接口API编程。依赖注入很好地通过protocol/interface降低了耦合度。

文章关联

依赖注入
泛型&协议
设计键盘

实现富文本多个link点击.

  1. 方案一:通过实现 shouldInteractWithURL 代理方法,判断不同的URL进行事件响应。 缺点:长按有类似网页的copy复选菜单弹出。
  2. 方案二:重写touchesBegan事件,分别做selectRange设置,拿到selectionRects(for: aRange), 通过记录rect集合和content. 点击触碰,拿到点判断在哪个集合中。再block事件回调。
  3. 方案三:自定义tap事件,关闭canBecomeFirstResponder. 识别点击哪一个link进行响应事件。这里重点讲一下方案3。以下为草稿代码:

自定义一个键盘

为什么要这样设计?

表单里引用键盘协议,XAppKeyboard协议定义在中间层,并且是遵循UIView。(为了设置TextField的inputView), 相关的功能api也定义在中间层。 实现层可import 对应的 journey library,可根据业务需要定制不同的键盘。比如车险投保键盘,金融账户的安全键盘等。WrappeKeyBoardView把具体的类型键盘进行包装,再注入到表单视图中。不同的target可根据开关配置决定是否启用。也可根据不同的场景注入不同的类型。好处就是具体的键盘代码不会和表单里代码耦合在一起。键盘的功能都可以在WrappedKeyBoardView里根据需要独立更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

git reset --soft HEAD~N 回退到上N个版本
git reset --soft head^ 将最近一次提交记录回退到暂存区
Git reset --mixed head^ 将最近一次提交回退到工作区
git reset --hard head^ 将最近一次提交记录清除
git reset --hard commitId 回退版本

git cherry-pick commitid 把其他分支的某提交记录复制到当前分支
git cherry-pick --abort 取消cherry-pick
git merge --abort 取消合并
git checkout . 重置当前所有修改(危险)

git revert (用一次新的提交回滚之前的提交且用于非合并类型的提交,而get reset 是直接删除指定的commit)
git revert head 撤销前一次提交
git revert -n commitid 撤销指定提交记录

git checkout -b test origin/test 本地创建一个和远程同名的分支
git pull origin test 更新test这个分支的代码
git checkout test 切换到test分支
git branch -a 查看远程分支
git branch -vv 查看分支关联
git status 查看当前状态
git log 查看提交日志

git pull 的意思是 git fetch 再 git merge FETCH_HEAD
git pull --rebase 的意思是 先 git fetch 再 git rebase FETCH_HEAD
git pull --autostash --rebase
git rebase -i 将本地的多次提交合并为一个(简化提交记录,合并到develop的时候看到的提交记录更加清爽)
git rebase -i HEAD~2 //合并前两个提交记录
git rebase 也可以用于合并。例如在开发分支执行git rebase develop,有冲突就解决冲突,解决后直接git add . 再git rebase --continue 即可。(git rebase 与 git merge 的原理不同,合并后并不会产生新的提交记录。)。 功能开发完成最后切到develop分支把功能进行合并进来 git merge xxx分支名。

git branch —delete branchName 删除本地分支
git push origin --delete test 删除远程分支

git tag 1.0.2 打tag
git push origin --tags 推送tag到远程
git push origin test1:test1 关联分支到远程分支

wechatSDK用在分享,支付等场景。 1.8.6 以上采取unverslink唤起app,当跳转失效时可以打开sdk日志开关来查看问题原因。

Xcode需要配置Associated Domains , 配置访问的域名。for example:
applinks:h5-vendor-prod.gaoying.com

Xcode 需要配置 apple-app-site-association 文件,该文件无后缀,里面是 json 配置。

1
2
3
4
5
6
7
8
9
10
11
{
"applinks": {
"apps": [],
"details": [
{
"appID": "yourDeveloperTeamId.com.livehome.com",
"paths": ["/livehome/*"]
}
]
}
}

wechatSDK 注册

1
2
3
4
5
6
7
let wechatFlag = WXApi.registerApp("wechatAppId", universalLink: "https://h5-vendor-prod.gaoying.com/livehome/")
NSLog("微信flag:\(wechatFlag)")
WXApi.startLog(by: .detail) { (logStr) in
NSLog("微信日志:\(logStr)")
}
let versionStr = WXApi.getVersion()
NSLog("微信sdk版本号:\(versionStr)")

需要到微信开放平台上设置 universLink。需要注意的是url必须要是https。

多Target场景配置:
iOS Project 多个 Target UniversLink 配置(付费文章)

谈到属性的原子性,即操作读写的原子性(安全的,单一的,完整的) 可以这么理解。那有什么方法可以做到?自然,我们想到了锁的概念。

锁的种类

我们在开发Apps的时候,经常会用到多线程。而锁的概念可以抽象成线程同步。主要作用即保护授权某段代码块的原子性执行。iOS中锁有很多种类:

  • 信号量 Semaphore (允许N个有限线程在某一时间执行指定代码块)
  • 线程锁 Mutex 保证在某一时间内执行某代码块只有一个线程进入。
  • 自璇锁 Spinlock 指的是一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到的情况下会循环等待,不断的判断。
  • 读写锁 Read-write-lock 并发只读,串行写。
  • 递归锁 Recursive-lock 这个锁可以被同一线程多次请求,而不会引起死锁。它主要是用在循环或递归操作中。

Grand Central Dispatch (GCD),CPU多核处理,线程间异步高效地执行任务。在GCD的管理下,只需提供DispatchWorkItem作为单个任务,本质上是个swift 闭包。这些任务项item由GCD根据一定地规则排队自动执行。通过 queue.async(execute: workItem)。通过GCD可设置任务的优先级,以及执行顺序。
几个任务项也可以作为一个group, 通过DispatchGroup组织多任务执行,最后将所有处理完毕的结果作为单一整体汇总。
队列Queue管理着任务项的执行,可串行也可并发。串行队列一次执行一个任务,并发队列无需等待执行中的任务结束才开启下一个任务(即可同时执行多项任务)。两种队列都按照先进先出(FOFO)执行。
内部,有GCD线程池服务所有的队列。当某任务执行完成后线程即销毁。当所有的线程属于busy状态,新的线程需要暂时挂起。GCD预备了五种队列可直接使用。一种串行队列:主队列。四种异步队列分别权限为:hign,default,low,background。

首先我们带着几点疑问去看文章:

  • 值类型(结构体、枚举) 和 引用类型 class 在内存中是如何存储的?
  • 值类型和引用类型性能上有什么不同?
  • 如果两者混合,会发生什么?比如struct 包含了 class 对象。
  • 到底该使用哪个呢?

值类型的定义

  • 值类型直接存储到栈中。每个值类型的变量持有自己的数据,之间互不影响。
  • 引用类型通过指针引用数据(存储在堆中),多个变量可指向同一数据,当操作某一变量时会影响其他变量。

值类型涉及到值拷贝,引用类型涉及到内存分配,引用计数。接下来我们大致了解一下内存段。

UserDefaults可以存储一些基础数据类型,如:Data,String,Date,Bool,Int,Double,Float,Array,Dictionary,URL,等基础对象。也可以存储自定义对象(但需要实现编码成Data进行存储)。
尽管如此,我们还是不推荐使用UserDefaults来存储数据量大的数据。因为读写非常昂贵,userDefaults最终用.plist文件进行存储。存储大数据会使此文件变得臃肿。
对于自定义的对象,我们可以通过写文件方式将json写到沙盒文件中。或者存储到coreData. Sqlite等。UserDefaults一般用于存储一些简单的数据类型,存储用户的偏好设置等。在写少读多的情况下,性能才会高。底层是通过读写xml文件方式都数据进行读写。

SUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension URLRequest {
static func search(term: String) -> URLRequest {
var components = URLComponents(string: "https://itunes.apple.com/search")
components?.queryItems = [
.init(name: "media", value: "music"),
.init(name: "entity", value: "song"),
.init(name: "term", value: "\(term)")
]

return URLRequest(url: components!.url!)
}
}

struct MusicService {

func search(_ term: String, completion: @escaping (Result<[Track], Error>) -> Void) {
URLSession.shared.dataTask(with: .search(term: term)) { data, response, error in
DispatchQueue.main.async {
completion(self.parse(data: data, error: error))
}
}.resume()
}

func parse(data: Data?, error: Error?) -> Result<[Track], Error> {
if let data = data {
return Result { try JSONDecoder().decode(SearchMediaResponse.self, from: data).results }
} else {
return .failure(error ?? URLError(.badServerResponse))
}
}
}

视图测试关注点:

  • tableview的行数是否正确?
  • label的文本显示是否正确?
  • button 是否启用或禁用?
  • view的frame是否正确?

viewController如果可以从他们依赖项隔离出来,那么就可以测试。依赖注入是一种可以把视图控制器隔离出来的技术。在测试中,我们可以用假数据替换依赖项,模拟真实数据的行为。
viewController有两个职责:渲染数据、响应用户交互。在MVVM设计模式中,控制器不主动从model中拉数据,也不负责从model中取出数据来更新控制器。