原文地址:Avoiding singletons in Swift
“我知道单例不好,但是…”,这是开发者在讨论代码时经常会说的一句话。单例模式用多了不好似乎已经成为了一种社区的共识,但与此人同时,包括苹果以及诸多第三方开发者在他们的内部应用以及开源框架中都在使用单例模式。
这周,让我们来看一下使用单例模式会遇到哪些问题以及如何通过一些技术来避免这些问题。让我们开始吧。
首页,我们要讨论一下为什么单例模式会这么流行。如果大多数开发者都认为单例模式是有害的,为什么还要持续不断的使用单例模式。
我想主要有两方面的原因。第一,我认为开发者在开发苹果平台应用时不断的使用单例模式的一个最大原因是因为苹果自己也在大量使用单例模式。作为第三方开发者,我们经常会期待苹果定义出所谓的“最佳实践”,任何苹果自己常用的的设计模式也都会在整个社区广泛传播。
第二点,我认为是便捷性。单例通常可以用作访问某些核心值或对象的捷径,因为它们基本上可以从任何地方被访问。看一下这个例子,我们想在ProfileViewController
中展示当前登录的用户的名字,并且可以点击按钮退出当前用户。
1 | class ProfileViewController: UIViewController { |
完成像上面这样的任务,非常方便(而且很常见)的做法就是使用UserManager
这样一个单例来封装用户和账号的处理逻辑。那么单例模式究竟有什么缺点呢?🤔
在讨论诸如模式和架构之类的问题时,我们很容易陷入过于理论化的陷阱。使我们的代码在理论上是“正确的“并遵循所有最佳实践和原则,这是很好的做好。但现实往往会打击我们,我们需要在两者之间寻求某种平衡。
那么单例模式通常会引起什么具体问题,为什么要避免这些问题? 我倾向于避免使用单例模式的三个主要原因是:
在之前的 ProfileViewController
示例中,我们已经可以看到这 3 个问题的迹象了。ProfileViewController
需要依赖UserManager
中的可选属性currentUser
,但是我们无法保证在编译时currentUser
一定有值,也就是当ProfileViewController
视图控制器展现时是否一定有数据。这种情况听起来像一个随时都可能发生的 Bug 😬!
与其让ProfileViewController
依赖单例对象,不如将这些对象注入到ProfileViewController
的初始化方法中。 在这里,我们将当前用户作为非可选对象,和一个用于执行注销操作的LogOutService
对象一起注入到ProfileViewController
中。
1 | class ProfileViewController: UIViewController { |
上面的代码看上去更加清晰,更易于管理。 现在,我们的代码可以安全地访问始终存在的对象,并且登出操作也有了很清晰的 API。 通常,为了使程序的核心对象之间的关系更加清晰,将各种单例和管理器重构为清晰的相互分离的服务是一种很好方法。
作为示例,让我们仔细看看LogOutService
是如何实现。 为了完成“注销”这一个操作,它依然使用依赖项注入的方式提供了一个清晰明确的 API。
1 | class LogOutService { |
从大量使用单例转变为充分利用服务,依赖项注入和本地状态可能需要花费很多时间,有时甚至需要巨大的重构。
幸运的是,我们可以运用Testing Swift code that uses system singletons in 3 easy steps提到的技术,使我们能够以更简单的方式摆脱单例。 就像在许多其他情况下一样-协议可以挽救!
无需一次性重构所有单例并创建新的服务类,我们可以简单地将服务定义为协议,如下所示:
1 | protocol LogOutService { |
然后,我们可能轻松的将单例改造为实现了这些协议的服务类。 在许多情况下,我们甚至不需要进行任何改动,只需将其共享实例作为服务传递即可。
同样的技术也可以用于改造我们应用程序中的其他”类似单例“的核心对象,例如使用 AppDelegate 进行导航。
1 | extension UserManager: LoginService, LogOutService {} |
现在,我们可以通过使用依赖项注入和服务的方式使所有视图控制器都不再依赖单例,而无需进行大量的重构! 然后,我们可以使用Replacing legacy code using Swift protocols中提到的技术逐一替换代码中的单例和其他类似单例的类型。
单例并不总是有问题的,但是在许多情况下,单例所导致的问题都可以通过在对象之间使用依赖注入的方式来避免这些问题。
如果您正在开发的项目中大量使用了单例模式,并且遇到一些相关的问题,那么希望这篇文章能对您有所启发。
]]>Collection
协议是 Swift 标准库中一个重要的协议,它定义了集合类型的一些特性。与Sequence
协议相比,Collection
协议拥有以下几个特点:Sequence
,Collection
的元素个数是有限for-in
迭代都是从头开始迭代。Collection
协议继承自Sequence
,可以说Collection
是一种特殊的Sequence
。下面就用一个具体的实例实现一个Collection
。
这里我们定义一个Class
用于表示一个班级,Student
用于表示班级里的一个学生。
1 | struct Student { |
使用如下:
1 | let s1 = Student(name: "aa", age: 10, no: 100) |
实现Collection
协议有几个关键的地方:
startIndex
与endIndex
两个计算属性的复杂度应该是 O(1)index(after:)
这个方法的实现,针对相同的输入必须要有相同的输出以上就是Collection
协议的介绍,Swift 标准库中还有很多其他协议,之后会一一介绍。
参考资料:
Collection
Everything You Ever Wanted to Know on Sequence & Collection
UserDefaults
是 iOS 中非常常用的一种数据持久化方式,在实际开始中我们一般不会直接使用系统提供的 API 去操作UserDefaults
,而是会创建一些扩张,利用扩展的方法来使用UserDefaults
。利用 Swift 语言的一些特性,我们可以写出非常优化的UserDefaults
扩展。利用 Swift 的泛型以及subscript
,可以实现一个不需要关心存储数据类型的UserDefaults
扩展,代码如下:
1 | extension UserDefaults { |
然后使用另外一个扩展定义 Key
1 | extension UserDefaults.Key { |
这样,在使用时就不需要关心类型了
1 | UserDefaults.standard[.likeCount] = 12 |
Swift5 中新增了Property Wrappers
特性,利用Property Wrappers
也可以使得UserDefaults
的使用变得非常优雅:
1 | struct UserDefault<T> { |
定义UserDefaults
的 Key
1 | struct UserDefaultsConfig { |
在使用时就可以直接赋值
1 | UserDefaultsConfig.likeCount = 13 |
上面两种方式唯一不同的点是,第一种方式每次都可以指定不同的defaultValue
,而第二种方式只能在定义属性时指定一次defaultValue
,使用时不能动态修改defaultValue
。
个人建议两种方式取其一即可,如果不需要动态修改defaultValue
,个人建议使用第二种方式,应为更加简洁明了。
参考资料:
Property wrappers to remove boilerplate code in Swift
Property Wrappers
The power of subscripts in Swift
for-in
语句进行访问,标准库中的Array
, Dictionary
, Set
等类型都实现了 Sequence 协议。下面介绍一下自定义类型如何实现 Sequence 协议。实现 Sequence 协议的集合有以下两个特点:
1 | struct Countdown: Sequence, IteratorProtocol { |
使用的时候就可以使用for-in
语句:
1 | let threeToGo = Countdown(count: 3) |
对于更为复杂的例子,需要实现自定义的Iterator
,下面的例子中实现了一个自定义的链表,并且自定义了迭代器LinkedListIterator
,这样链表就可以使用for-in
语句进行访问
1 | class LinkedList<E: Equatable> { |
参考资料:
Sequence
Everything You Ever Wanted to Know on Sequence & Collection
在 SwiftUI 中使用 UIKit 需要用到UIViewRepresentable
和UIViewControllerRepresentable
两个协议,分别对应在 SwiftUI 中使用UIView
和UIViewController
。
在 SwiftUI 中使用UIView
需要实现UIViewRepresentable
协议,如下代码实现了在 SwiftUI 的List
控件中使用UITableViewCell
1 | struct TableViewCell: UIViewRepresentable { |
在 SwiftUI 中使用UIViewController
需要实现UIViewControllerRepresentable
协议,如下示例展示了怎么从一个 SwiftUI 页面跳转到UIViewController
。
1 | struct MyViewController: UIViewControllerRepresentable { |
在 UIKit 中使用 SwiftUI 要比在 SwiftUI 中使用 SwiftUI 简单很多,只需要使用UIHostingController
对 SwiftUI 的控件进行一下包装就可以,代码如下:
1 | let vc = UIHostingController(rootView: Text("Hello World")) |
包装出来的结果是一个UIViewController
,这样就可以在其他UIView
或UIViewController
中使用了。可以将这个UIViewController
作为一个childViewController
加到其他的UIView
或UIViewController
中去
总体方案是 Nginx + Gunicorn,Nginx 负责代理转发,Gunicorn 是一个 WSGI 容器,负责启动应用。我使用的是阿里云 Ubuntu 系统。
1 | 更新源 |
启动之后,在浏览器上输入服务器的外网 IP 就可以看到 Nginx 的界面
1 | 删除Nginx默认配置文件 |
配置文件内容:
1 | server { |
解释:127.0.0.1:8000 是 flask 应用运行的本地地址,gunicorn 运行应用默认使用 8000 端口。
pipenv 是 Python 的虚拟运行环境。
1 | curl https://raw.githubusercontent.com/kennethreitz/pipenv/master/get-pipenv.py | python |
进入 flask 项目目录,运行虚拟环境
1 | pipenv shell |
在项目目录下创建config.py
,填入内容
1 | accesslog = "/root/log/access.log" # 访问日志文件 |
1 | gunicorn -c config.py release:app |
release 是项目入口文件包名,app 为入口文件中的应用名,即 Flask 对象的名称。
上面就是整个部署流程,这过程中遇到的问题有如下几个:
1 | # 使用Python3.7创建虚拟环境 |
以上就是我部署 Flask 项目的整个流程,其实 Python 项目的部署方案有很多种,这只是其中的一种方案。
]]>除了 iOS,2018 年自己还确实学了不少东西,下面我大概列举了一下自己 2018 年学的东西:
虽然学了很多,但是没有太多实践的机会,所有很多东西学的并不是很深入。
另外,这些东西基本都是在 10 月份之前学的,10 月份开始,由于公司新项目启动,自己进入了疯狂加班状态,学习基本处于半停滞状态。10 月份到 12 月份基本没有学习什么新的东西。
另外,由于新项目采用 Swift 语言开发,自己在 Swift 语言上也有了一些经验。
2018 年已经是我在懒人听书呆的第 4 个年头了,说实话,我当初也没有想到自己能呆这么久。
今年工作上最大的事情就是从 9 月份开始,我作为技术经理,全权负责公司新项目芽芽故事 iOS 端的开发工作。经过团队(3 个人)一起的不懈努力,最终于 12 月中旬,按时完成了开发任务,没有延期。这是我在懒人第一次完整的负责一个 App 的整个开发过程,从最开始的立项,架构设计,技术选型,再到模块划分,人员分配等。这个过程中自己确实得到了很大的锻炼。不光是技术上的,还包括沟通,协调,管理等能力也得到了很大的提升。
年初定的减肥计划,上半年执行的很好,到 8 月份时最低体重降到了 67kg。不过,经过 10 月,11 月,12 月的加班,又反弹到了 72kg,看来明年还得继续努力啊。
9 月份跟公司一起去日本旅游了一趟,算是了了自己的一桩心愿。
投资方面,股市暴跌,基金赔了将近 1 万块。上半年 P2P 暴雷,虽然自己投的平台没有出事,但还是吓得接连减仓。下半年和朋友合伙投资了一家便利店,最终被合伙人坑了,赔了将近 4 万,加上基金赔的,基本上把自己去年在数字货币上赚的又都赔进去了,只能怪自己阅历太少,不知人心险恶。
上面就是我 2018 年的总结,总的来说,2018 年自己的表现并不是很好,年初的很多目标没有完成,留给自己的时间已经不多了,毕竟年龄在那摆着,已经是快奔三的人了。希望自己继续努力加油,在 2019 年能取得更大的进步。
]]>1 | def decorator(func): |
1 | def decorator(func): |
1 | def decorator(func): |
func(*args, **kw)
这种形式调用,因此为了通用,装饰器的内部函数wrapper
的参数就可以为这种形式1 | def log(text): |
1 | a = [1, 2, 3, 4, 5, 6, 7, 8] |
1 | # BookCollection就是一个自定义的可迭代对象 |
解决只能遍历一次的问题
1 | import copy |
1 | def gen(max): |
1 | a = [] |
__len__
与__bool__
方法有关__bool__
时会调用__len__
来判断对象时 True 还是 False,__len__
返回 0 或 False,则对象为 False,否则对象为 True__len__
__bool__
方法,则由__bool__
方法来控制对象时 True 还是 False,__bool__
只能返回 Bool 类型,__len__
不再被调用__name__
会被改变1 | import time |
解决办法: 使用 wraps
1 | import time |
1 | try: |
__closure__
里1 | def curve_pre(): |
1 | def f1(): |
1 | def factory(pos): |
1 | # 普通函数 |
1 | a = 12 |
1 | def fun(a): |
1 | from functools import reduce |
1 | from functools import reduce |
我们是上周五(2018-9-14)早上 9 点出发,到东京成田机场是下午 3 点左右。刚到机场就感受到了很多不一样的东西。
首先是随处可见的自动贩卖机
跟国内不一样的饮料
到酒店已经是下午 5 点了,刚到酒店把行李一放,我们就迫不及待的要出去逛逛。
先是去吃饭,说了半天,总算用我们蹩脚的文给店员讲明白了我们要吃什么
吃完饭就开始逛,没走多远就遇见了一个 SEGA 的游戏厅,进去感受了一下,很有氛围
一楼是各种抓娃娃机,抓什么的都有,模型,手办
二楼就是各种游戏机,跟国内的游戏厅不太一样,这些游戏机基本上都是面向成年人的,店内基本上没有小孩子
还看见了女神异闻录 5,哈哈,P5 天下第一啊!!!
FGO
游戏厅逛完后,我们就继续沿着街道走,这时天已经渐渐黑了。我们找了一家居酒屋,进去点了几个菜,由于语言不通,闹出了很多笑话,本来我们想每人点一杯酒,结果店员理解成了只点 1 杯酒,另外居酒屋的烤串跟国内的很不一样,没有辣椒和自然,只是有一层甜甜的酱汁,而且拷的肉并不是全熟,我觉得顶多只有 5 分熟,不过口感很不错
之后又去了另外一家人气很旺的居酒屋,不过点的菜我们真的有点吃不惯,酒也喝不惯,总之体验一般。
这就是我们第一天的行程,夜晚的东京(准确说应该是成田市)还是很热闹的。
第二天的行程是迪士尼,总体来说还是玩的很开心的,几个比较刺激好玩的项目都玩了
晚上回到酒店又出去逛街了,这次又体验了两家居酒屋,这次的体验很不错,尝到了日本拉面,日本清酒,玉子烧等,非常好吃
吃完了逛了个超市,然后就回酒店了,第二天结束。
第三天我们先是去了一个公园,然后去了人眼八海,下午就直奔富士山。
日本的乡村,街道非常干净。
中午的午餐,炸鸡排,非常好吃
乡村的自助贩卖摊位
人眼八海,水非常清
偷拍一个日本小萝莉
逛完了人眼八海,我们就直奔富士山,刚到富士山还云雾缭绕,什么也看不清,不一会,云开雾散,远处的富士山非常清晰
来张自拍,在日本这几天又胖了几斤啊
看完了富士山我们就直接回酒店了,今天住的酒店是温泉酒店,但说实话温泉真的不怎么样,体验非常差。第三天就这样结束了。
第四天就是购物之旅,早上先去了两家免税店,这个没啥看的,然后去了新宿,在新宿吃了寿喜烧,看了艺伎表演
歌舞伎町一番街
新宿也是满满的二次元味道
寿喜烧
艺伎表演,我们几个一直在讨论表演者到底是个男的还是女的,直到走的时候也没看出来
接着我们就直奔秋叶原,这是本次旅游我最期待的行程
FGO 巨大的宣传画
世嘉的游戏厅
到处的洋溢着二次元的气氛
传说中的女仆咖啡厅,只不过时间有限,没能进去体验一下(其实是钱包有限)
Cosplay
秋叶原呆了两个小时,然后又去了银座,说实话,银座对我来说真的没什么逛的,我也没什么东西要买的
街上遇到了一个街头表演,我把仅剩的几个硬币全给了他
银座逛完,吃完饭我们就回酒店了,第四天就结束了。
第五天就是回家。
总结一下这次旅游的感受,日本给我最大的印象就是干净,不管是繁华的大街上还是乡下的小路上,都非常干净,而且他们的垃圾分类真的做的非常好,导游一再提醒我们扔垃圾要分开仍,在国内即使碰到了分类垃圾箱也是随便扔,但这几天旅游却不由自主的做到了垃圾分类。另外一点就是日本的服务员非常热情,不管你是去便利店,商城,大超市,居酒屋还是其他什么地方,服务员见到你第一句都是“欢迎光临”,非常热情。其他的就没什么了,能去到秋叶原也算是圆了我的一个梦想。这次由于时间比较短,很多想去的地方没有去,希望下次有机会还能再去日本好好逛逛。
]]>abc
\d, \D, \f, \n
等,每种元字符都有不同的含义或
的关系^
[a-z], [a-zA-Z]
1 | s = 'abc, acc, adc, aec, afc, ahc' |
\d
代表数字,相当于[0-9]
\D
代表非数字,相当于[^0-9]
\w
代表单词字符,相当于[0-9a-zA-Z_]
\W
代表非单词字符,相当于[^0-9a-zA-Z_]
\s
代表空白字符\S
代表非空白字符.
代表除换行符的所有字符[a-z]{3,6}
匹配 3 到 6 次1 | # 匹配3-6个字母,贪婪模式下,会匹配尽可能多的字母,直到不满足条件,因此会匹配出['python', 'java', 'php'] |
*
匹配前面的字符 0 次或 n 次+
匹配前面的字符 1 次或 n 次?
匹配前面的字符 0 次或 1 次,要与非贪婪模式的问号区分^
首部,^000
开头三个字符为000
$
尾部,000$
结尾三个字符为000
(Python){3}
,把一个组重复 3 次re.I
.
表示匹配任意字符: re.S
1 | s = 'abc, acc, adc, aec, afc, ahc, aFc' |
1 | a = 'python 111java678php' |
1 | s = 'life is short, i use python, i love python' |
1 | import json |
1 | s1 = json.dumps(a) |
1 | from enum import Enum |
1 | from enum import Enum |
1 | print(VIP.YELLOW) # VIP.YELLOW,枚举类型 |
1 | class VIP(Enum): |
from package.module import Student
1 | class Student(): |
1 | class Student(): |
1 | class Student(): |
__dict__
保存着对象的所有变量以及变量的值1 | class Student(): |
1 | class Person(): |
self.name
Student.sum
, self.__class__.sum
1 | class Student(): |
1 | class Student(): |
Student.sum1
1 | class Student(): |
__name
,则该方法或变量名是私有的_Student__name
,_Student__private_method
1 | class Student(): |
1 | class Student(): |
Student.method(s)
,虽然可以这样做,但不推荐这样做Human.__init__(self, name, age)
super(Student, self).__init__(name, age)
推荐使用这种方法1 | class Human(): |
1 | def funcname(parameter_list): |
return value
,如果没有 return,则默认返回 NonePython 中关于递归函数:
sys.setrecursionlimit(10000)
方法来修改递归次数关于 print 函数:
1 | a = b = c = 1 |
1 | def func(num): |
1 | a, b, c = 1, 2, 3 |
1 | def add(num1, num2): |
1 | def add(num1, num2, num3=3, num4=4): |
*
*
1 | def fun(*param): |
**
1 | def fun(**param): |
1 | c = 50 # 全局变量 |
1 | def fun(): |
与其他语言不通,/
是除法,得到的 float 类型,要得到整型需要用//
1 | type(2/2) # float |
1 | 0b10 # 2进制 |
布尔类型有两个值,True
与False
True
,0 可以转为False
False
,其他字符串可以转为True
False
,序列中有元素可以转为True
False
,字典或序列中有元素可以转为True
False
1 | num1 = 1 |
'My name is "ljl"'
"Let's go"
多行文本可以用三个双引号表示,也可以用三个单引号表示,例如:
1 | ''' |
print
函数在字符串前加一个 r,字符串中的转义字符不会被解析
1 | print(r'aaa \n aaa') # aaa \n aaa |
1 | s1 = "Hello" |
1 | name = "Liujinlong" |
1 | a = [1, 2, 3] |
1 | l1 = [1, 2, 3, 4] |
1 | (1, 2, 3, True) |
1 | print('or' in 'Hello world') # True |
len([1, 2, 3])
max([1, 2, 3]), min([1, 2, 3])
1 | {1, 2, 3, 4, 5} |
1 | empty = {} # 空字典 |
id()
: 显示变量内存地址1 | a = [1, 2, 3] |
+-*/
//
%
**
例如,2**2 = 4
=, +=, -=, *=, /=, %=, **=, //=
==, !=, >=, >, <=, <
and, or, not
2 and 1 # 1
1 and 2 # 2
1 or 2 # 1
0 or 1 # 1
2 and 1
,更具左边的元素 2 无法判断出结果,读取 1 后,可以判断出结构,所有返回 1,1 or 2
根据左边的元素 1 就直接可以判断出表达式的值,所以直接返回 1in, not in
type(a) == int
isinstance(a, int), isinstance(a, (int, float, str))
特殊的地方:
1 | a = 1 |
1 | # List Comprehension |
__init__.py
文件包名.模块名
__init__.py
文件也是一个模块导入一个模块有两种方法,import 和 from-import
import package1
package1.a
1 | import test |
from module import a
from package.module import a
from package import module
__all__
from test import a, b, c
1 | from test import a |
__input__.py
文件__init__.py
会被自动执行,可以做一些初始化的工作__init__.py
的应用场景:1 | # __init__.py 包名:t |
可以使用 dir()可以打印出模块里的所有变量,dic(module)
打印模块中所有的变量
__name__
: 模块的完整名称,包括包名package.module
__package__
: 模块所属的包名__file__
: 模块文件的完整路径__doc__
: 模块的注释1 | # module1 |
如果一个模块被当成应用程序执行的入口(直接通过pyton xxx.py
执行),则上面的几个变量就会变。
__name__
变为__main__
__package__
为空__file__
为是执行路径与模块文件名的相对路径,与执行路径有关1 | print("__name__ :" + (__name__ or "None")) |
__name__
的应用1 | if __name__ == '__main__': |
上面的语句是标明的 Python 文件既可以作为模块,也可以作为 app 的入口文件
import package1.package2.module
或from package1.package2 import module1
from .m3 import *
,相对导入只能用于from-import
__main__
这个模块是不存在的,除非使用python -m
示例,如下图目录结构:
要在main.py
中引入其他模块,不能使用相对导入,只能使用绝对导入:
1 | # main.py |
在module1_1_2.py
中导入其他模块可以使用相对导入
1 | from . import module1_1_1 |
上面要注意的一点是,package1
中的模块引用package2
中的模块时,只能用绝对导入,因为package1
和package2
都是和入口文件main.py
同级的
未完待续!
]]>1 | ViewController *vc = [[ViewController alloc] init]; |
modalTransitionStyle是一个枚举,有以下四个值,分别代表四种不同的转场效果,
1 | UIModalTransitionStyleCoverVertical = 0, |
如果上面四种效果都不能满足你的需求,还可以使用CATransition进一步定制你的转场动画,代码如下:
1 | ViewController *vc = [[ViewController alloc] init]; |
使用CATransition的type和subtype两个属性可以定制16中转场动画,同时CATransition还可以设置转场持续的时间(duration)以及动画速度控制函数(timingFunction)等属性。注意,一定要把动画加载控制器view的window上。
完成代码,包括present和dismiss
1 | ViewController *vc = [[ViewController alloc] init]; |
如果CATransition的16种转场动画还不能满足你的需求,iOS7之后,可以使用UIViewControllerAnimatedTransitioning协议完全自定义转场动画,还能实现手势控制转动画。具体代码可以参考我写的这个Demo。
以上就是我对模态界面转场动画的一个总结。
]]>我调查了一下市面上一些常见应用的做法,基本上可以总结出三种做法:
比较常见的做法是,在h5页面内定义一个JS的全局变量或者json,指定分享用的图片地址,APP内,通过JavaScriptCore获取全局变量,然后用于分享。
具体这样做的应用有:Keep,哔哩哔哩(部分页面),京东(部分页面)
还有一种是把分享的信息定义在页面头部(head标签里),QQ音乐就是这样做的。
如果H5页面是第三方提供,就不能通过第一种方法去指定分享内容了。这种情况下,大多数应用的做法就是取页面内第一张符合大小的图片作为分享图片。
这里我写了一个JS函数,只要把这个函数注入到H5页面中,APP就可以通过调用这个方法来获取到第一张符合大小的图片。
1 | function getImage(width, height) { |
具体这样做的应用有:微信,今日头条
有些应用在分享时会对H5页面进行截图,然后把截图作为分享的内容。
具体这样做的应用有:微博,UC浏览器
以上就是三种获取H5页面内分享缩略图的三种策略,我们最终选择的是第一种和第二种相结合的方式,如果页面内指定了分享图片,则使用,否则获取页面内第一张符合条件的图片进行分享。
]]>git rebase,也叫做变基,也是Git种一种合并代码的手段,与git merge不同的是,rebase是直接将一个分支从他们的共同父节点开始后的所有Commit依次合并到另外一个分支上。如下图:
两个分支develop和master,如果通过命令git merge
将develop分支合并到master分支,Git会将‘C’,‘3’以及两个分支的共同父节点‘B’进行三路合并,合并完成后生成一个新节点,如下图:
如果git rebase
将develop分支变基到master分支上,Git会将develop分支上的所有commit(1,2,3)依次合并到master,每一次合并都会生成一个新的提交(如下图1’,2’,3’),合并完成后,如下图:
可以明显的看到,使用git rebase
,分支线依然保持为一条,分支线看起来也没有那么乱,这也是git rebase
相对于git merge
的一个优点,但是另一方面,git rebase
在合并每一个提交并生成一个新的提交时,会改写原来提交的提交时间(不会改写提交人),而且git rebase
也没有留下合并的痕迹,可追溯性没那么强。
在使用git rebase
的过程中经常会遇到这种情况,在执行的git rebase
操作后,遇到了一个冲突,修改冲突后执行git rebase --continue
,然后又来一个冲突,冲突一个接一个,而且有时候同一个冲突会出现好几次。
其实,这是由rebase的原理造成的,Git在合并每一个Commit时都会判断是否有冲突,如下图:
两个分支develop和master以及他们各自的文件内容,现在要将develop变基到master上,Git会先将develop的第一个commit和master分支的最新提交合并,合并后如下:
会报一个冲突,解决完冲突后,执行git rebase --continue
,根据冲突不同的解决方案,可能会与不同的结果,如下图:
上图中的两种冲突解决方案,在执行git rebase --continue
后,依然再会报一个冲突,应为在合并develop的第二个提交时,依然有冲突。下面的解决方案则不会造成再次冲突,因为这种解决方案是完全用develop分支覆盖了master分支,如下图:
以上就是造成git rebase
冲突太多的具体原因。
冲突太多怎么办,Git提供了一个辅助工具git rerere
命令,具体用法可以参考git rerere。rerere命令能够记住解决一个冲突的方法,这样在下一次看到相同冲突时,Git 可以为你自动地解决它。
那么rerere能够避免git rebase
带来的冲突吗,答案是否定的。因为rerere在判断两个冲突是否为相同冲突是根据冲突体的两部分是否完全一样来进行的,就上面的例子而言,不管是哪种冲突解决方案,第一次和第二次冲突的冲突体都不完全一样,因此rerere都不会自动帮我们修复。
而且,rerere命令也会记住错误的冲突解决方案,下次遇到相同的冲突时会直接应用错误的方案。不过你可以使用git rerere forget <pathspec>
命令来删除Git记住的冲突解决方案。
说了这么多,那rerere命令到底有什么用呢?
git rerere
命令为我们提供了一种减少冲突的方案:当你要保证一个长期分支会干净地合并,但是又不想要一串中间的合并提交。 将rerere功能打开后偶尔合并,解决冲突,然后返回到合并前。 如果你持续这样做,那么最终的合并会很容易,因为rerere可以为你自动做所有的事情。git rerere
也可以将冲突的解决方案共享给项目组的其他成员。
所以说rerere命令还有一点用的,不过他并不能彻底解决冲突多的问题,减少冲突还是需要我们在平时使用时规范git的使用方法,使用统一的git工作流。
本来打算写一篇的,但是因为太长了,所以分为两篇来写。由于写的比较仓促,文中如果有什么纰漏,欢迎指出。
参考资料:
Pro Git
Why do I have to resolve the same conflict over and over?
git merge
和git rebase
是我们平时在使用Git过程中用到比较多的两个命令。本文将主要介绍这个两个命令的基本用法以及使用中应该注意的事项。顾名思义,git merge
是用来将一个分支的代码合并到另一个分支。如下图,将develop分支上的代码合并到master分支上。
上图中有一个Fast-forward
字样,这是Git的一种合并方式。
如上图,在master分支的B点时牵出一个develop分支,develop分支又有了3个新的提交1,2,3,而master分支此时没有新的提交,这是如果合并的话,develop分支不用动,master只用把分支的头指针指向develop的最新的一个提交3即可。合并后的结果如下图:
这个合并方式看似把两个分支合并了,实际上并没有真正进行合并操作,也没有留下合并的操作。默认git merge
是采用Fast-forward
方式进行合并的,如果不想采用这种方式,可以在命令后面加入--no-ff
选项,如下:
不使用Fast-forward
的方式进行合并,Git会为合并生成一个新的提交,合并后的结果如下图:
Commit C即是Git自动生成的合并提交。一般情况下我们在合并代码时都会加上--no-ff
,这样可以更清晰的看见合并操作。
如下图:
在牵出develop分支后,master分支又有了新的提交C,这时候如果把develop分支合并到master,就不能简单的通过移动master分支头指针来进行了,这时候默认执行的是recursive(递归)的策略进行合并的,这种策略下,Git会对两个分支的头结点(C与3)以及他们的共同父节点(B)进行三路合并,这种情况下就可能出现代码冲突。
而且Git在合并两个分支时,并不会根据Commit的提交时间来判断哪个分支的代码更新,这就可能造成一个隐藏的问题:
想象一下这种场景,一个团队内分为两个小组分别开发A和B两个功能版本,A版本的开发人员修改了某个文件(假定是file1),B版本的的开发人员发现他也要对file1做同样的修改才能继续开发,于是他对file1做了同样的修改(或者使用cherry-pick把A版本的相应的commit拉过来)。过了一段时间,A版本的开发人员发现他以前对file1的修改有问题,于是又把file1改了回去,而这时候B版本的开发人员并没有做同样的修改。这样,将来A版本和B版本合并到主干分支后,B版本的代码就会覆盖A的修改,于是错误的代码又被合并到了主干分支上。下面就是这个过程的示意图:
如图,序号1,2,3表示的是Commit的提交顺序,FeatureA分支在commit 2上把文件内容改为了bbb111
, FeatureB分支把文件内容改为了bbb222
,在commit 6上FeatureA有吧内容改回了AAA
,而B分支没有做同样的修改,当把FeatureA合并到主干时,由于FeatureA相对于共通父节点Commit 1来说没有变化,因此就会使用FeatureB的内容作为最终的内容,而且这种情况下不会报任何冲突。这就极有可能造成错误的代码又被合并到了主干分支上,而且这种问题极难被察觉,就好像莫名其妙的发现代码丢失了一样。
要避免这种情况的发生,只能从流程上来规范git的操作。如果一个修改要应用到多个分支上,应该单独为这个修改建立一个临时分支,修改完成后,每一个需要用到这个修改的分支都合并这个临时分支。将来如果这个临时分支又有了新的提交,依然是每个分支都要合并。这样就可以避免出现上面的情况。
未完待续。
]]>本文的主要内容就是对整个适配过程做一个总结。网上有很多详细的教程,具体的适配过程本文就不再赘述了,这里主要总结一下我在适配过程中越到的问题以及一些经验。
适配完成后可以使用自建证书进行测试,关于如何创建自建证书,网上有很多教程,可以自行百度。如果你使用的是AFNetworking框架,使用自建证书也非常简单,首先见证书(abc.cer)导入项目,然后加入代码如下:
1 | self.sessionManager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; |
当然,正式环境还是需要使用CA机构颁发的证书,使用CA机构颁发的证书不需要导入证书,只需要加入以下代码即可:
1 | self.sessionManager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; |
项目中一些不能使用HTTPS协议的请求,需要配置NSExceptionDomains。很多第三方服务的请求还没有支持HTTPS,这就需要我们把这些请求添加到NSExceptionDomains中。可以使用Charles抓包看有哪些第三方请求需要添加到NSExceptionDomains中。
另外,为了防止域名劫持,项目中可能会直接使用IP进行请求,我再测试过程中发现,即使不添加NSExceptionDomains,直接使用IP也可以正常请求。
对于WebView里的请求,可以NSAllowsArbitraryLoadsInWebContent字段来声明绕过ATS。
对于多媒体播放请求(使用AVFoundation),可以使用NSAllowsArbitraryLoadsInMedia字段来声明绕过ATS。
一般公司服务器都会有多个环境(测试环境,正式环境等),证书一般只配置在正式环境上,这就要求我们在测试环境下需要关闭ATS。如果每次都手动修改Info.plist文件来关闭ATS,不但麻烦,也可能导致配置的信息丢失。一种比较好做法是创建多个Info.plist,根据不同的环境,自动切换使用不同的Info.plist。
如下,创建多个Info.plist
然后在Build Setting中配置不同的环境使用不同的Info.plist
这样做有一个不好的地方是如果修改了Info.plist文件,需要同时修改两个。但这也比每次切换环境修改Info.plist要好,因为毕竟修改Info.plist的情况还是比较少的。
以上就是我在适配HTTPS过程中遇到的一些问题,希望对大家有所帮助。
参考资料:
]]>最近由于升级了AFNetworking,在使用NSURLProtocol过程中发现了一个问题,就是在拦截到POST请求后,HTTPBody是空的。以前使用旧版本的AFNetworking时是没有这问题的。分析了一下,新版的AFNetworking使用的是NSURLSession,旧版使用的是NSURLConnection,可能是由于这个原因导致的。网上查了一下,还真有这个问题,具体可以看这个问题以及这里的讨论。
网上有人提出了一种解决方案,就是不要使用HTTPBody,而使用HTTPBodyStream。具体实现如下:
1 | NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:url]; |
通过这个方法就可以获得HTTPBody的内容。
+registerClass:
方法只适用于sharedSession
另外一个要注意的地方就是,只用在使用[NSURLSession sharedSession]
时,注册NSURLProtocol才能使用+registerClass:
方法,否则就需要使用NSURLSessionConfiguration
来注册NSURLProtocol,代码如下:
1 | NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; |
因此,对于新版的AFNetworking,由于它使用的不是sharedSession
,所以就不能简单的通过类方法+registerClass:
来注册自定义NSURLProtocol,也必须通过NSURLSessionConfiguration
来设置。代码如下:
1 | NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; |
以上就是使用NSURLProtocol时要注意的两个问题,希望能对大家有所帮助。
]]>