Fork me on GitHub
乔羽的技术博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

Swift写时复制

发表于 2019-01-22 | 更新于 2019-03-04 | 阅读次数: | 字数:1.8k | 阅读时长:7min

为什么会有写时复制

为什么引入值类型

在Swift中,Apple引入了值类型,相对于引用类型,值类型更加的高效和安全。

  1. 值类型存储在栈上,引用类型存储在堆上,效率比较高

  2. 使用值类型,有可能在编译阶段就把问题暴露出来。比如

    1
    2
    3
    4
    5
    6
    7
    let arr1 = NSMutableArray.init(array: ["a","b","c"], copyItems: false)
    arr1.removeObject(at: 0)
    //以上编译不会报错,由于arr1是引用类型,存储的是指针,指针不变就没问题,对指向的对象没有限制

    let arr2 = ["a","b","c"]
    arr2.remove(at: 0)
    //以上编译会报错,由于arr2是值类型

值类型,即每个实例保持一份数据拷贝。

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值类型。而平时使用的 Int, Double, Float, String, Array, Dictionary,Set 其实都是用结构体实现的,也是值类型。

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

如果声明一个值类型的常量,那么就意味着该常量是不可变的(无论内部数据为 var/let)。

这样看来一切都没问题,但是在写代码的时候,这些值类型每次赋值的时候真的是重新在内存中拷贝一份吗?如果一个数组里存了上万个元素,现在把它赋值给另一个变量,就必须要拷贝所有元素,即使这两个数组的内容是完全一致的,那可以预见这对性能会造成多么糟糕的影响。

既然我们能够想到这样的问题,那苹果的工程师肯定也想到了。如何才能避免不必要的复制呢,Swift给出了优化方案:Copy-On-Write(写时复制),即只有当这个值需要改变时才进行复制行为。在Swift标准库中,Array、Dictionary和Set都是通过写时复制来实现的。

什么是写时复制?

1
2
3
var x = [1,2,3]
var y = x
// 断点

这个时候我们打印一下 x 和 y 的内存地址,这里我用的是 lldb 命令fr v -R [object] 来查看对象内存结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) fr v -R x
(Swift.Array<Swift.Int>) x = {
_buffer = {
_storage = {
rawValue = 0x0000600001c0ac40 {
......省略无用信息
}
}
}
}
(lldb) fr v -R y
(Swift.Array<Swift.Int>) y = {
_buffer = {
_storage = {
rawValue = 0x0000600001c0ac40 {
......省略无用信息
}
}
}
}

由此我们可以看到 x 和 y 的内存地址都是0x0000600001c0ac40,说明 x 和 y 此时是共享同一个实例。这个时候我们再加上下面的代码:

1
2
y.append(4)
// 断点

然后再打印一下地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) fr v -R x
(Swift.Array<Swift.Int>) x = {
_buffer = {
_storage = {
rawValue = 0x000060000126b180 {
......省略无用信息
}
}
}
}
(lldb) fr v -R y
(Swift.Array<Swift.Int>) y = {
_buffer = {
_storage = {
rawValue = 0x0000600002301b60 {
......省略无用信息
}
}
}
}

可以看到 x 和 y 的内存地址不在相同了,说明此时它们不再共享同一个实例,y 进行了数据拷贝。

Array 结构体内部含有指向某块内存的引用。这块内存就是用来存储数组中元素。x 和 y 两个数组一开始是共享同一块内存。不过,当我们改变 y 的时候,这个共享会被检测到,内存中的数据被拷贝出来,改变以后赋值给了 y。昂贵的元素复制操作只在必要的时候发生,这就是写时复制。

内部实现

在结构体内部存储了一个指向实际数据的引用,在不进行修改操作的普通传递过程中,都是将内部的引用的引用计数+1,在进行修改时,对内部的引用做一次copy操作,再在这个复制出来的数据上进行真正的修改,从而保持其他的引用者不受影响。

值类型嵌套引用类型

上面我们提到的 Array 内部元素是 Int,两者都是值类型,那么如果 Array 内部的元素是引用类型呢,情况会不会发生变化?我们一起来看一下~

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
class Student {
var name: String
init(name: String) {
self.name = name
}
}

struct School {
var student = Student(name: "小明")
}

let school1 = School()
var school2 = school1
print(school1.student.name)
print(school2.student.name)
// 断点1

school2.student.name = "小红"
print(school1.student.name)
print(school2.student.name)
// 断点2

school2.student = Student(name: "小李")
print(school1.student.name)
print(school2.student.name)
// 断点3

输出结果:
小明
小明
小红
小红
小红
小李

断点1,school1 和 school2 的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(lldb) fr v -R school1
(Copy_On_WriteTest.ViewController.School) school1 = {
student = 0x00006000024053e0 {
name = {
_guts = {
_object = {
_object = 0x9000000105a02356
}
}
}
}
}
(lldb) fr v -R school2
(Copy_On_WriteTest.ViewController.School) school2 = {
student = 0x00006000024053e0 {
name = {
_guts = {
_object = {
_object = 0x9000000105a02356
}
}
}
}
}

当school1 赋值给school2 后,school1.student和school2.student的内存地址都是0x00006000024053e0,其引用类型实例变量 name 的地址也都是 0x9000000105a02356 ,它们共享同一个实例,其引用类型的实例变量也共享。

1.修改结构体内引用类型的实例变量的值

断点2,school1 和 school2 的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(lldb) fr v -R school1
(Copy_On_WriteTest.ViewController.School) school1 = {
student = 0x00006000024053e0 {
name = {
_guts = {
_object = {
_object = 0x9000000105a0235c
}
}
}
}
}
(lldb) fr v -R school2
(Copy_On_WriteTest.ViewController.School) school2 = {
student = 0x00006000024053e0 {
name = {
_guts = {
_object = {
_object = 0x9000000105a0235c
}
}
}
}
}

而执行school2.student.name = "小红" 后,school1.student 与 school2.student 的内存地址不变,其实例变量 name 内存地址都发生改变且相同,还是共享同一个实例变量,也就是说,虽然对值类型有所修改,但没有发生拷贝行为。

2.修改结构体内引用类型的值

断点3,school1 和 school2 的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(lldb) fr v -R school1
(Copy_On_WriteTest.ViewController.School) school1 = {
student = 0x00006000024053e0 {
name = {
_guts = {
_object = {
_object = 0x9000000105a0235c
}
}
}
}
}
(lldb) fr v -R school2
(Copy_On_WriteTest.ViewController.School) school2 = {
student = 0x0000600002405400 {
name = {
_guts = {
_object = {
_object = 0x9000000105a02362
}
}
}
}
}

school2.student 和school2.student.name 的内存地址都发生变化,而school1.student 和school1.student.name 的内存地址不变,说明,此时对结构体进行了拷贝行为,而student 这个引用类型是直接指向另一个实例,而不是对原实例进行修改。

自定义Struct如何实现写时复制

作为一个结构体的作者,你并不能免费获得这种特性,你需要自己进行实现。当你自己的类型内部含有一个或多个可变引用,同时你想要保持值语义,并且避免不必要的复制时,为你的类型实现写时复制是有意义的。

在 Swift 中,我们可以使用 isKnownUniquelyReferenced 函数来检查某个引用只有一个持有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct School {
private var student = Student(name: "小明")

var studentName: String {
get {
return student.name
}
set {
if isKnownUniquelyReferenced(&student) {
student.name = newValue
}
else {
student = Student(name: newValue)
}
}
}
}

WKWebView进阶填坑指南-Swift

发表于 2018-11-15 | 阅读次数: | 字数:1.8k | 阅读时长:7min

Cookie处理

在设置Cookie的时候,我们经常做的是在请求的请求头里添加Cookie,但是这只是把Cookie发送给了服务端,我们本地并没有保存Cookie,Cookie最终要写到WebView的一个Cookie文件目录里面,后续WebView里面自己的发起的请求或者跳转才能在发起请求的时候在对应的域名下面取到Cookie传出去。

Webview加载 H5 页面,实际上是把页面相关的.html、js、css文件下载到本地,然后再加载,这时页面去获取Cookie 的时候,是去本地WebView里的Cookie文件目录里查找,如果没有设置的话肯定就找不到了。所以在设置Cookie的时候,服务端和客户端都要设置。

一、服务端 Cookie 设置

在使用UIWebView的时候,我们是通过NSHTTPCookieStorage来管理 Cookie的,下面我们给devqiaoyu.tech这个域名添加一个名为user的Cookie。

1
2
3
4
5
6
7
8
9
10
var props = Dictionary<HTTPCookiePropertyKey, Any>()
props[HTTPCookiePropertyKey.name] = "user"
props[HTTPCookiePropertyKey.value] = "admin"
props[HTTPCookiePropertyKey.path] = "/"
props[HTTPCookiePropertyKey.domain] = "devqiaoyu.tech"
props[HTTPCookiePropertyKey.version] = "0"
props[HTTPCookiePropertyKey.originURL] = "devqiaoyu.tech"
if let cookie = HTTPCookie(properties: props) {
HTTPCookieStorage.shared.setCookie(cookie)
}

WKWebView Cookie问题在于WKWebView发起的请求不会自动带上存储于NSHTTPCookieStorage容器中的Cookie。

解决办法也很简单,就是在WKWebView发起请求之前,先从NSHTTPCookieStorage读取Cookie,然后手动往URLRequest的请求头里添加一下Cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getCookie() -> String {
var cookieString = ""
if let cookies = HTTPCookieStorage.shared.cookies {
for cookie in cookies {
if cookie.domain == cookieDomain {
let str = "\(cookie.name)=\(cookie.value)"
cookieString.append("\(str);")
}
}
return cookieString
}

var request = URLRequest(url: URL(string: "https://devqiaoyu.com"))
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")

当服务器页面发生重定向的时候,此时第一次在RequestHeader中写入的Cookie会丢失,还需要对重定向的请求重新做添加Cookie的处理。具体方法请往下看~

二、客户端 Cookie 设置

上面这么写完了,当页面加载的时候,后端无论是啥语言,都能从请求头里看到Cookie了,但是后端渲染返回页面后,在客户端的WebView里运行的时候,JS 在执行的时候调用document.cookie API 是读取不到Cookie的,所以还得针对客户端Cookie进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cookieString = ""
if let cookies = HTTPCookieStorage.shared.cookies {
for cookie in cookies {
if cookie.domain == "devqiaoyu.tech" {
let str = "\(cookie.name)=\(cookie.value)"
cookieString.append("document.cookie='\(str);path=/;domain=devqiaoyu.tech';")
}
}
}
let cookieScript = WKUserScript(source: cookieString, injectionTime: .atDocumentStart, forMainFrameOnly: false)
let userContentController = WKUserContentController()
userContentController.addUserScript(cookieScript)

let webViewConfig = WKWebViewConfiguration()
webViewConfig.userContentController = userContentController

let webV = WKWebView(frame: CGRect.zero, configuration: webViewConfig)

客户端Cookie注入实际上就是创建一个 JS 脚本,让WebView去执行,推荐在.atDocumentStart这个时机进行预置静态 JS 的注入。这样WebView在加载后端返回的静态页面的时候,就可以拿到保存着客户端的Cookie了。

注意:document.cookie() 无法跨域设置 Cookie,比如你第一次加载的请求时 www.baidu.com ,在重定向的时候跳转到了 www.google.com ,那么第二个请求就可能因为没有携带 Cookie而无法访问。当然啦,解决办法还是有的,请往下看~

URL拦截

在WKWebView中,每一次页面跳转之前,都会调用下面的回调函数:

1
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)

Web 页面重定向问题

重定向问题有两种:

  • 服务器页面重定向,需要对新发起的请求重新种Cookie
  • 本地页面重定向,只要客户端设置了Cookie,那么就不需要处理了

所以如果是服务器页面重定向,那么判断此时Request是否有你要的Cookie没有就Cancel掉,修改Request重新发起。

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
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
{
var shouldCancelLoadURL = false
if let cookie = navigationAction.request.value(forHTTPHeaderField: "Cookie") {
if cookie.contains("user") {
shouldCancelLoadURL = false
} else {
var request = URLRequest(url: URL(string: (navigationAction.request.url?.absoluteString)!)!)
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")
webView.load(request)
shouldCancelLoadURL = true
}
} else {
var request = URLRequest(url: URL(string: (navigationAction.request.url?.absoluteString)!)!)
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")
webView.load(request)
shouldCancelLoadURL = true
}

if shouldCancelLoadURL {
decisionHandler(WKNavigationActionPolicy.cancel)
} else {
decisionHandler(WKNavigationActionPolicy.allow)
}
}

跨域问题

针对跨域的问题,解决办法和上面的方法类似,仅仅是判断条件不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
{
var shouldCancelLoadURL = false
if let url = navigationAction.request.url?.absoluteString {
if url.contains("devqiaoyu.tech") { // 原来的域名
shouldCancelLoadURL = false
} else {
// 重新发起请求,种Cookie
shouldCancelLoadURL = true
}
} else {
// 重新发起请求,种Cookie
shouldCancelLoadURL = true
}

if shouldCancelLoadURL {
decisionHandler(WKNavigationActionPolicy.cancel)
} else {
decisionHandler(WKNavigationActionPolicy.allow)
}
}

假跳转的请求拦截

一种 JS 调用 Native 的通信方案,详细介绍可以看从零收拾一个hybrid框架(一)– 从选择JS通信方案开始。下面内容是从该文章内摘录的。

何谓 假跳转的请求拦截 就是由网页发出一条新的跳转请求,跳转的目的地是一个非法的压根就不存在的地址,比如

1
2
3
4
//常规的Http地址
https://wenku.baidu.com/xxxx?xx=xx
//假的请求通信地址
wakaka://wahahalalala/action?param=paramobj

看我下面写的那条假跳转地址,这么一条什么都不是的扯淡地址,直接放到浏览器里,直接扔到WebView里,肯定是妥妥的什么都打不开的,而如果在经过我们改造过的Hybrid WebView里,进行拦截不进行跳转

url 地址分为这么几个部分

  • 协议:也就是 http/https/file 等,上面用了wakaka
  • 域名:上面的 wenku.baidu.com 或 wahahalalala
  • 路径:上面的 xxxx? 或 action?
  • 参数:上面的 xx=xx 或 param=paramobj

如果我们构建一条假url

  • 用协议与域名当做通信识别
  • 用路径当做指令识别
  • 用参数当做数据传递

客户端会无差别拦截所有请求,真正的 url 地址应该照常放过,只有协议域名匹配的 url 地址才应该被客户端拦截,拦截下来的 url 不会导致 WebView 继续跳转错误地址,因此无感知,相反拦截下来的 url 我们可以读取其中路径当做指令,读取其中参数当做数据,从而根据约定调用对应的 Native 原生代码

以上其实是一种 协议约定 只要 JS 侧按着这个约定协议生成假 url,Native 按着约定协议拦截/读取假 url,整个流程就能跑通。

User-Agent设置

全局设置

就是App内所有Web请求的User-Agent全部被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UIWebView
let webView = UIWebView(frame: CGRect.zero)
let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")
if let agent = userAgent {
let user = "@\(agent);extra_user_agent"
let dict = ["UserAgent":user]
UserDefaults.standard.register(defaults: dict)
}

// WKWebView
let webV = WKWebView(frame: CGRect.zero)
webV.evaluateJavaScript("navigator.userAgent") { (result, error) in
if let oldAgent = result as? String {
let user = "@\(oldAgent);extra_user_agent"
let dict = ["UserAgent":user]
UserDefaults.standard.register(defaults: dict)
}
}

单个WebView设置

在iOS9,WKWebView提供了一个非常便捷的属性去更改User-Agent,就是customUserAgent属性。这样使用起来不仅方便,也不会全局更改User-Agent,可惜的是iOS9才有,如果适配iOS8,还是要使用上面的方法。

1
2
3
4
5
6
let webView = UIWebView(frame: CGRect.zero)
let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")
if let agent = userAgent {
let user = "@\(agent);extra_user_agent"
webView.customUserAgent = user
}

参考文章

WKWebView 那些坑

纯Swift项目基础第三方库

发表于 2018-11-02 | 更新于 2018-11-17 | 阅读次数: | 字数:179 | 阅读时长:1min

网络框架

  • Moya - 用 Swift 写的网络抽象层
  • Alamofire

Keychain

  • KeychainAccess

JSON解析

  • HandyJSON - 一个方便的swift json-object 序列化/反序列化库

UserDefaults

  • SwiftyUserDefaults - NSUserDefaults 的现代化 Swift API

图片解析加载

  • Kingfisher - 是一个轻量、纯 Swift 的库,可以从网络中异步下载和缓存图片

加解密

  • CryptoSwift - 是一个不断扩充的集合,包含了标准、安全、用 Swift 实现的加密算法

布局

  • Snapkit

富文本

  • AttributedLabel - 显示性能数量级 UILabel 的 AttributedLabel

键盘类

  • IQKeyboardManagerSwift

提示框

  • Toast-Swift

下拉刷新

  • PullToRefreshKit

SegmentControl

  • HMSegmentedControl-Swift

空页面

  • DZNEmptyDataSet-Swift - 空页面视图框架

TabBarController

  • RDVTabBarControllerSwift - 一个定制化的TabBarController库,可动画显示隐藏tabbar栏,可定制tabbar栏

HUD

  • PKHUD

使用Carthage发布自己创建的Framework并使用

发表于 2018-11-01 | 阅读次数: | 字数:623 | 阅读时长:2min

Xcode 9.4.1

创建Framework

1.新建工程

打开Xcode,command+n新建工程,选择Cocoa Touch Framework,点击next,命名为StringExtension。

新建Framework

2.创建文件

StringExtension.swift是不准备暴露出去的,Test.swift是准备暴露出去给外界调用的。

StringExtension

test

需要暴露出来给外界调用的类或方法必须要加上关键字public。

3.将Scheme设置为shared

scheme

4.使用Carthage构建Framework

终端进入工程目录下,执行下面代码

1
$ carthage build --no-skip-current

carthage build

执行完成以后会发现,目录下多了一个Carthage文件夹,我们的Framework就在该文件夹里面。

framework

将Framework上传至Github

1.新建仓库

new repository

2.初始化仓库

终端进入工程目录下,执行下面代码

1
2
3
4
5
6
$ echo "# StringExtension" >> README.md
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin https://github.com/Geselle-Joy/StringExtension.git
$ git push -u origin master

setup.png

3.添加gitignore

1
2
3
4
$ touch .gitignore #这句话只是创建了gitignore文件,记得要打开添加内容
$ git add .gitignore
$ git commit -m "add gitignore"
$ git push

4.添加工程文件

1
2
3
$ git add .
$ git commit -m "add StringExtension"
$ git push

5.添加版本标记

1
2
$ git tag 1.0.0
$ git push --tags

到这里我们自己创建的framework已经上传到github上了。

调用我们创建的Framework

1.用Xcode创建新项目

创建项目,cd到工程目录

2.创建一个空的carthage文件

1
$ touch Cartfile

3.编辑Cartfile文件

编辑Cartfile文件,输入如下内容

1
github "Geselle-Joy/StringExtension"

版本含义

1
2
3
4
5
~> 3.0 表示使用版本3.0以上但是低于4.0的最新版本,如3.5, 3.9
== 3.0 表示使用3.0版本
>= 3.0 表示使用3.0或更高的版本

如果你没有指明版本号,则会自动使用最新的版本

4.保存并关闭Cartfile文件,在终端执行命令

1
$ carthage update --platform iOS

安装过程如下

1
2
3
4
5
$ testss carthage update --platform iOS
*** Cloning StringExtension
*** Checking out StringExtension at "1.0.0"
*** xcodebuild output can be found in /var/folders/8w/s8bn_8c56p9czz75nqdqmp2m0000gn/T/carthage-xcodebuild.ecw6rz.log
*** Building scheme "StringExtension" in StringExtension.xcodeproj

carthage会为你下载和编译所需要的第三方库,当命令执行完毕,在你的项目文件夹中会出现一个名为Carthage的文件夹和Cartfile.resolved的文件。

在~/Carthage/Build/iOS文件夹里就可以看到我们自己创建的StringExtension.framework了。

5.导入framework

打开你的项目,选择target, 再选择上方的General,将需要的framework文件拖到 Linked frameworks and Binaries内。

import framework

项目已添加到Git上以后再添加gitignore

发表于 2018-11-01 | 阅读次数: | 字数:185 | 阅读时长:1min

接手别人的项目的时候,总是发现gitignore并没有设置或者设置的有问题,所以需要对gitignore进行添加或更新。

1.为避免冲突,先拉取下远程仓库

1
git pull

2.清除本地目录下的缓存

1
git rm -r --cached .

3.新建或更新.gitignore文件

在项目的根目录下创建.gitignore文件,并添加相应的过滤规则。

1
vim .gitignore

这里推荐一个网站:gitignore.io,可以根据你项目的需要获取gitignore

或者也可以去 https://github.com/github/gitignore 上找对应的gitignore

4.再次添加所有文件到本地仓库缓存中

1
git add .

5.添加commit

1
git commit -m "add gitignore"

6.提交到远程仓库

1
git push

如何验证生产环境Push推送

发表于 2018-10-22 | 阅读次数: | 字数:180 | 阅读时长:1min

在测试推送的时候,发现在测试环境测试的时候,是没有问题的,不论APP在前台还是在后台都可以收到推送,但是在生产环境,一直收不到推送。查了查资料后了解到,在生产换成测推送的时候,需要生成Ad-Hoc Profile。

生成Ad-Hoc Profile

生成Profile,下载安装

Ad-Hoc

更改Scheme

  • Product —>Scheme —>Edit Scheme
  • 更改Build Configuration为Release

scheme

更改Signing

选中自己刚才生成的Profile

signing

打包

Product —> Archive,等打包完成后,Window —> Organizer,选择你刚才打的包。

archive

选择Export。

export

选择Ad Hoc。然后一步一步导出ipa包。拿到ipa包以后,使用itools安装至真机上,开始测试。

升级Xcode10后遇到的问题

发表于 2018-10-11 | 更新于 2018-10-16 | 阅读次数: | 字数:1.7k | 阅读时长:9min

1. Multiple commands produce

1
2
3
4
5

Multiple commands produce '/Users/joy/Library/Developer/Xcode/DerivedData/FKY-cpdtnacrrykispgfanfwxzyhrnge/Build/Products/Debug-iphonesimulator/FKY-TEST.app/Info.plist':
1) Target 'FKY-TEST' (project 'FKY') has copy command from '/Users/joy/Documents/1药网Git/fangkuaiyi_ios/FKY/FKYSwift/Vendor/SKPhotoBrowser/Info.plist' to '/Users/joy/Library/Developer/Xcode/DerivedData/FKY-cpdtnacrrykispgfanfwxzyhrnge/Build/Products/Debug-iphonesimulator/FKY-TEST.app/Info.plist'
2) Target 'FKY-TEST' (project 'FKY') has copy command from '/Users/joy/Documents/1药网Git/fangkuaiyi_ios/FKY/Supporting Files/Info.plist' to '/Users/joy/Library/Developer/Xcode/DerivedData/FKY-cpdtnacrrykispgfanfwxzyhrnge/Build/Products/Debug-iphonesimulator/FKY-TEST.app/Info.plist'
3) Target 'FKY-TEST' (project 'FKY') has process command with output '/Users/joy/Library/Developer/Xcode/DerivedData/FKY-cpdtnacrrykispgfanfwxzyhrnge/Build/Products/Debug-iphonesimulator/FKY-TEST.app/Info.plist'

报错原因:重复

方案一

修改 Xcode 的配置暂时解决了编译报错的问题。

Xcode -> File -> Workspace settings -> Build System,选择 Legacy Build System 。

方案二

  1. 选择打开所在的项目Target -> Build Phases -> Copy Bundle Resources
  2. 删除报错的那些文件

2. library not found for -lstdc++.6.0.9

Xcode10不支持libstdc++系列的库了、需要删掉并且换成libc++系列的库。有些第三方由于没有更新,会牵扯到这个库,所以运行后报错。

方案一

打开Build Phases -> Link Binary With Libraries,删除lstdc++.6.0.9依赖

有可能还有删除Build Setting -> Other Linker Flags里的-l"stdc++.6.0.9"

但是现在部分第三方SDK依旧要依赖于lstdc++.6.0.9.tbd动态库,如果对代码进行改动或者修改 Xcode 的配置,可能会造成其他问题,例如在Xcode9.4上再次运行的时候可能会出现报错等等。

方案二

最简单解决该问题的方法就是从 Xcode9.4 中找到动态库lstdc++.6.0.9.tbd、libstdc++.6.tbd、libstdc++.tbd文件复制到 Xcode10 下就可以完美解决问题。

  • 真机

    1
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/
  • 模拟器

    1
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/

libstdc++文件下载链接

3. ld: symbol(s) not found for architecture x86_64

前面两个问题解决后,就遇到了这个问题,目前,在模拟器运行的时候会报这个错误,而使用真机运行的话没有问题,猜测应该是iOS12 模拟器的问题。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ld: warning: ignoring file /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk/usr/lib/libstdc++.6.0.9.tbd, missing required architecture x86_64 in file /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk/usr/lib/libstdc++.6.0.9.tbd (3 slices)
ld: warning: ignoring file /Users/joy/Documents/1药网Git/fangkuaiyi_ios/FKY/Vendors/AipOcrSdk/IdcardQuality.framework/IdcardQuality, missing required architecture x86_64 in file /Users/joy/Documents/1药网Git/fangkuaiyi_ios/FKY/Vendors/AipOcrSdk/IdcardQuality.framework/IdcardQuality (3 slices)

Undefined symbols for architecture x86_64:
"std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::string::assign(std::string const&)", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::basic_fstream<char, std::char_traits<char> >::open(char const*, std::_Ios_Openmode)", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::basic_fstream<char, std::char_traits<char> >::close()", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::string::_Rep::_S_empty_rep_storage", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::ios_base::Init::~Init()", referenced from:
__GLOBAL__sub_I_BVMDOfflineMapMerge.cpp in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::basic_fstream<char, std::char_traits<char> >::~basic_fstream()", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::string::_Rep::_M_destroy(std::allocator<char> const&)", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::ostream::tellp()", referenced from:
_baidu_framework::MergeDataPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, _baidu_framework::tagVectorMapFile&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::ostream::write(char const*, long)", referenced from:
_baidu_framework::MergeNamePart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeIndexPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeDataPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, _baidu_framework::tagVectorMapFile&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::istream::seekg(long long, std::_Ios_Seekdir)", referenced from:
_baidu_framework::MergeIndexPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeDataPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, _baidu_framework::tagVectorMapFile&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::CheckPatchMd5(std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::ios_base::Init::Init()", referenced from:
__GLOBAL__sub_I_BVMDOfflineMapMerge.cpp in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::basic_fstream<char, std::char_traits<char> >::basic_fstream()", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::istream::tellg()", referenced from:
_baidu_framework::MergeNamePart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeIndexPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeDataPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, _baidu_framework::tagVectorMapFile&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::CheckPatchMd5(std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::__throw_length_error(char const*)", referenced from:
std::vector<_baidu_vi::CVPtrRef<_baidu_framework::CTextureData>, std::allocator<_baidu_vi::CVPtrRef<_baidu_framework::CTextureData> > >::_M_fill_insert(__gnu_cxx::__normal_iterator<_baidu_vi::CVPtrRef<_baidu_framework::CTextureData>*, std::vector<_baidu_vi::CVPtrRef<_baidu_framework::CTextureData>, std::allocator<_baidu_vi::CVPtrRef<_baidu_framework::CTextureData> > > >, unsigned long, _baidu_vi::CVPtrRef<_baidu_framework::CTextureData> const&) in BaiduMapAPI_Map(TextureDataLoader.o)
"std::istream::read(char*, long)", referenced from:
_baidu_framework::MergeNamePart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeIndexPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::MergeDataPart(std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, std::basic_fstream<char, std::char_traits<char> >&, _baidu_framework::tagVectorMapFile&, int) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::CheckPatchMd5(std::basic_fstream<char, std::char_traits<char> >&) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
"std::basic_filebuf<char, std::char_traits<char> >::is_open() const", referenced from:
_baidu_framework::HandleBasemapMerge(_baidu_vi::CVString const&, _baidu_vi::CVString const&, _baidu_vi::CVString const&, char const*) in BaiduMapAPI_Map(BVMDOfflineMapMerge.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Mac下用Python写iOS自动化测试

发表于 2018-10-10 | 更新于 2018-11-30 | 阅读次数: | 字数:1.3k | 阅读时长:5min

安装环境:

  • MacOS Mojave 10.14
  • Xcode10.1

安装libimobiledevice

网上查到的教程

移除所有的 iOS 设备,然后在终端输入以下代码

1
2
3
4
5
6
7
brew uninstall ideviceinstaller
brew uninstall libimobiledevice
brew install --HEAD libimobiledevice
brew link --overwrite libimobiledevice
brew install --HEAD ideviceinstaller
brew link --overwrite ideviceinstaller
sudo rm -rf /var/db/lockdown/*

连接一台iOS设备,并信任,然后输入以下代码

1
sudo chmod -R 777 /var/db/lockdown/

查看手机信息

1
ideviceinfo -d

查看手机上安装的所有APP的 BundleId

1
ideviceinstaller -l

实际安装过程

遇到问题

在运行brew install --HEAD libimobiledevice这段代码时遇到问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Last 15 lines from /Users/joy/Library/Logs/Homebrew/libimobiledevice/01.autogen.sh:
checking dynamic linker characteristics... darwin18.0.0 dyld
checking how to hardcode library paths into programs... immediate
checking for pkg-config... /usr/local/opt/pkg-config/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
checking for libusbmuxd >= 1.1.0... no
configure: error: Package requirements (libusbmuxd >= 1.1.0) were not met:

Requested 'libusbmuxd >= 1.1.0' but version of libusbmuxd is 1.0.10

Consider adjusting the PKG_CONFIG_PATH environment variable if you
installed software in a non-standard prefix.

Alternatively, you may set the environment variables libusbmuxd_CFLAGS
and libusbmuxd_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details.

看上去是libusbmuxd的版本不对。在网上查了很多方案以后,发现只能编译安装1.1.0版本。

编译安装

编译安装的时候需要依赖于很多库,不过由于我一开始使用brew install --HEAD libimobiledevice的时候,自动给我安装好了依赖的库,所以就不需要我再一一编译安装了。

编译安装libusbmuxd-1.1.0
1
2
3
4
5
git clone https://github.com/libimobiledevice/libusbmuxd.git
cd libusbmuxd
./autogen.sh
make
sudo make install
编译安装libimobiledevice
1
2
3
4
5
git clone https://github.com/libimobiledevice/libimobiledevice.git
cd libimobiledevice
./autogen.sh
make
sudo make install

在运行 ./autogen.sh 又遇到问题:

1
2
checking for openssl >= 0.9.8... no
configure: error: OpenSSL support explicitly requested but OpenSSL could not be found

我确认我安装的openssl版本都是1.0以上,但还是报openssl版本错误。在网上查到,依赖库可以使用openssl或GnuTLS,所以我决定用另一个试一试。

1
brew install GnuTLS

安装完依赖库以后,输入以下代码重新编译安装

1
./autogen.sh --disable-openssl

又遇到一个新的问题:

1
configure: error: libgcrypt is required to build libimobiledevice with GnuTLS

需要安装libgcrypt库

1
brew install libgcrypt

安装完成后,重新编译安装,OK,No problem~

编译安装ideveceinstaller

我本来想用 HomeBrew 安装,结果发现在 HomeBrew 安装 ideveceinstaller 时,它依赖于libimobiledevice,会自动下载某个稳定版的 libimobiledevice ,所以最好还是只能编译安装。

1
2
3
4
5
git clone https://github.com/libimobiledevice/ideviceinstaller.git
cd ideviceinstaller
./autogen.sh
make
sudo make install

自动化测试

安装WebDriverAgent

真机安装WebDriverAgent

ATX 文档 - iOS 真机如何安装 WebDriverAgent

从GitHub上下载代码

1
git clone https://github.com/facebook/WebDriverAgent

运行初始化脚本

1
./Scripts/bootstrap.sh

该脚本会使用Carthage下载所有的依赖,使用npm打包响应的js文件

执行完成后,直接双击打开WebDriverAgent.xcodeproj这个文件。

设置好Team、Profile、Signing Certificate以后,选择真机,Product -> Test。

一切正常的话,手机上会出现一个无图标的WebDriverAgent应用,启动之后,马上又返回到桌面。这是很正常的不要奇怪。

此时控制台界面可以看到设备的IP。

通过上面给出的IP和端口,加上/status合成一个url地址。例如http://10.0.0.1:8100/status,然后浏览器打开。如果出现一串JSON输出,说明WDA安装成功了。

而inspector的地址是http://localhost:8100/inspector, inspector 是用来查看UI的图层,方便写测试脚本用的

使用终端替代Xcode

通常来说为了持续集成,能够全部自动化比较好一些

1
2
3
4
5
6
7
8
9
# 解锁keychain,以便可以正常的签名应用,
PASSWORD="replace-with-your-password"
security unlock-keychain -p $PASSWORD ~/Library/Keychains/login.keychain

# 获取设备的UDID
UDID=$(idevice_id -l | head -n1)

# 运行测试
xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination "id=$UDID" test

安装Python3 wda

1
pip install --pre facebook-wda

这个时候就可以来写python脚本了

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
38
39
40
41
42
43
import time
import wda

c = wda.Client(url='http://169.254.16.231:8100')

index = 0

def main():
# 打开测试APP
with c.session('com.jifen.qukan') as s:
# 此时的s就是当前打开app的回话对象,我们可以通过它来操作app的内容
# 具体的语法可以在wda的README.md里面找到,这里就不说了
if s(type='Button', name=u'视频').exists:
s(type='Button', name=u'视频').tap()

time.sleep(3)

while(True):
pushSubVC(s)

def pushSubVC(s):
global index
es = s(label='home content ext').find_elements()
i = 1
for e in es:
print(i)
i+=1
d = es[index]
rect = d.bounds
x = rect.x
y = rect.y
index=index+1
s.click(x,y-20)
time.sleep(63)
if s(label='home content back white').exists:
d = s(label='home content back white')
d.tap()
if s(type='ScrollView').exists:
d = s(type='ScrollView')
d.scroll(direction='down',distance=0.4)

if __name__ == '__main__':
main()

参考文章

安装libimobiledevice

获取手机已安装app的bundleid

安装和使用ideviceinstaller时遇到的坑

libimobiledevice安装(Mac)

编译安装libimobiledevice

自动化测试

使用Python写iOS自动化测试

iOS WebDriverAgent 环境搭建

iOS+Python+Appium真机自动化测试实战

Python + Appium+ IOS自动化测试

MAC下搭建 appium+ios+python 自动化测试环境

正则表达式验证

发表于 2018-08-22 | 更新于 2018-09-27 | 分类于 开发 | 阅读次数: | 字数:3k | 阅读时长:16min

是否为电话号码【简单写法】

1
2
3
4
5
6
7
8
9
10
11
12
13
/*!
* 是否为电话号码【简单写法】
*
* @param pattern 传入需要检测的字符串
*
* @return 返回检测结果 是或者不是
*/
+(BOOL)GS_isPhoneNumber:(NSString *)phoneNum
{
NSString *MOBILE = @"^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$";
NSPredicate *regextestmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", MOBILE];
return [regextestmobile evaluateWithObject:phoneNum];
}
阅读全文 »

OC优秀的第三方库

发表于 2018-07-28 | 更新于 2018-09-27 | 分类于 开发 | 阅读次数: | 字数:1.2k | 阅读时长:4min

0C优秀的第三方库

视频

  • XYWSoundChanger - iOS视频变声器,对录制或下载的视频进行变声,对视频原声处理成大叔、萝莉、搞怪等效果

  • KTVVideoProcess - 来自Changba iOS团队的高性能视频效果处理框架

  • YGPlayer - iOS video player.(iOS播放器)

  • KRVideoPlayer - 36氪开源的视频播放器

音频

  • DOUAudioStreamer - 豆瓣开源的音频播放器

AR

  • HeavenMemoirs - AR相册

  • ARKit-CoreLocation - 将AR的高精度与GPS数据的比例结合起来

阅读全文 »
12…4
乔羽 / FightingJoey

乔羽 / FightingJoey

Efforts to become the person I want to be.

36 日志
2 分类
9 标签
RSS
GitHub 简书 掘金 微博
我的朋友
  • 败寇路飞
© 2015 — 2020 乔羽 / FightingJoey
本站访客数: