分类目录归档:Swift

Swift语言的随机数:运行效率和安全性(cryptographically good)

Apple推出Swift语言已经很多年了,如今,Swift语言已经进化到了5.1版本,很多细节也越来越完善了,不过,总的来说,Swift语言的overhead仍然较大,具体到随机数上,Swift内置的运行时生成随机数效率低,不适用于模拟,测试和游戏等对于cryptographically good要求不高的场景。

如果读者需要大量生成随机数的话,建议用arc4random或者arc4random_uniform,不要使用arc4random_buf或者Swift语言的random方法。对于测试,模拟和游戏等不需要cryptographically good/secure的情况,可以直接引入C语言类库的rand方法,运行效率更高。

Swift对于内置的Double, Int等类型提供了random方法,用以生成指定的开闭区间内的随机数。然而,经过实际测试,我发现,一方面,Swift语言本身的overhead很大,另一方面,Swift内置的SystemRandomNumberGenerator使用的是arc4random_buf,该方法比arc4random和arc4random_buf慢很多。所以,如果效率非常重要,那么,请不要使用Swift类库的随机数,应该在Swift中调用C/C++语言实现的随机数库。


       var i = 0;
        while(i<100000000){
 //1亿个随机数,每个随机数在闭区间[0,11]内取值,随机数独立同分布
            let _ = arc4random_uniform(11)
            i += 1;
        }
       var i = 0;
        while(i<100000000){
//1亿个随机数,每个随机数在闭区间[0,2^32-1]内取值,随机数独立同分布
            let _ = arc4random()
            i += 1;
        }
       var i = 0;
        while(i<100000000){
            var b:UInt32 = 0;
//1亿个随机数,每个随机数在闭区间[0,2^32-1]内取值
            arc4random_buf(&b, 4)
            i += 1;
        }
        var i = 0;
        while(i<100000000){
            var b:UInt32 = 0;
            b = UInt32.random(in: 0...10)
            i += 1;
        }

以下为一个特别简单的模n同余的Linear Congruent伪随机数生成器,实际测试发现,生成1亿个32位随机数的话,Debug模式下需要耗时29.31秒, Release模式下需要5.06秒。

struct LCG: RandomNumberGenerator {
    var seed:UInt64
    
    init() {
        self.seed = mach_absolute_time()
    }
    
    init(seed:UInt64) {
        self.seed = seed;
    }
    
    mutating func next() -> UInt64 {
        self.seed = 32479 &+ self.seed
        return self.seed
    }
    
}

var i = 0;
while(i<100000000){
    var b:UInt32 = 0;
    b = UInt32.random(in: 0...10, using: &lcg)
    i += 1;
}

经过实际测试,结果如下
1亿个随机数,

随机数生成方法 debug release
rand 11.21秒 0.52秒
arc4random 3.09秒 2.34秒
arc4random_uniform 3.41秒 2.67秒
arc4random_buf 19.69秒 18.79秒
UInt32.random 47.98秒 19.27秒
自定义LCG 29.31秒 5.06秒

UInt32.random实际上会使用Swift内置的SystemRandomNumberGenerator,实际上最终调用了arc4random_buf,然而,与直接调用arc4random_buf相比,UInt32.random在Debug模式下慢了近30秒,Release模式下则慢了16秒,因此,我们可以得出结论,即便是Release模式下,Swift的overhead还是很大的。因此,如果需要高效率生成很多随机数的话,不建议直接使用Swift的Double, Int的random方法。不过很特别的是,rand函数在Debug模式下竟然比arc4random慢很多,甚至和arc4random_buf差不多了,但是,在Release模式下,rand还是最快的,0.52秒就可以生成1亿个伪随机数。

接下来,我们来看看为什么arc4random_buf比arc4random和arc4random_uniform慢这么多。

经过查看苹果的具体实现 https://opensource.apple.com/source/Libc/Libc-1272.200.26/gen/FreeBSD/arc4random.c.auto.html

我们不难发现,arch4random方法的具体代码如下,其中CACHE_LENGTH的值为64

uint32_t
arc4random(void)
{
	int ret;
	os_unfair_lock_lock(&arc4_lock);
	arc4_init();
	if (arc4_count <= 0) {
	    arc4_stir();
	}
	if (cache_pos >= CACHE_LENGTH) {
		ret = ccdrbg_generate(&rng_info, rng_state, sizeof(rand_buffer), rand_buffer, 0, NULL);
		os_assert_zero(ret);
		cache_pos = 0;
	}
	uint32_t rand = rand_buffer[cache_pos];
	// Delete the current random number from buffer
	memset_s(rand_buffer+cache_pos, sizeof(rand_buffer[cache_pos]), 0, sizeof(rand_buffer[cache_pos]));
	arc4_count--;
	cache_pos++;
	os_unfair_lock_unlock(&arc4_lock);
	return rand;
}

而arc4random_uniform实际调用arc4random方法,并且通过多次循环避免modulo bias

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by trying successive ranges of bits from the random
 * value, each large enough to hold the desired upper bound, until a range
 * holding a value less than the bound is found.
 */
uint32_t
arc4random_uniform(uint32_t upper_bound)
{
	if (upper_bound < 2)
		return 0;

	// find smallest 2**n -1 >= upper_bound
	int zeros = __builtin_clz(upper_bound);
	int bits = CHAR_BIT * sizeof(uint32_t) - zeros;
	uint32_t mask = 0xFFFFFFFFU >> zeros;

	do {
		uint32_t value = arc4random();

		// If low 2**n-1 bits satisfy the requested condition, return result
		uint32_t result = value & mask;
		if (result < upper_bound) {
			return result;
		}

		// otherwise consume remaining bits of randomness looking for a satisfactory result.
		int bits_left = zeros;
		while (bits_left >= bits) {
			value >>= bits;
			result = value & mask;
			if (result < upper_bound) {
				return result;
			}
			bits_left -= bits;
		}
	} while (1);
}

也就是说,arc4random和arc4random_uniform由于rand_buffer[64]的存在,每生成64个32位随机数,才需要调用一次ccdrbg_generate,而arc4random_buf每一次调用,都需要调用一次ccdrbg_generate。所以,如果读者需要大量生成随机数的话,建议用arc4random或者arc4random_uniform,不要使用arc4random_buf或者Swift语言的random方法。

对于测试,模拟和游戏等不需要cryptographically good/secure的情况,可以直接引入C语言类库的rand方法,运行效率更高。

Swift语言的兼容性问题

从Swift 3开始,可以认为Swift源代码级别兼容是没有问题的了,即Swift 4的编译器可以直接正确编译Swift 3的代码,而不需要像Swift 2到3或者Swift 1到2那样必须进行代码迁移。

原始信息在这:Swift Evolution,特别注意下面这段话

The primary goal of this release is to solidify and mature the Swift language and development experience. While source breaking changes to the language have been the norm for Swift 1 through 3, we would like the Swift 3.x (and Swift 4+) languages to be as source compatible with Swift 3.0 as reasonably possible. However, this will still be best-effort: if there is a really good reason to make a breaking change beyond Swift 3, we will consider it and find the least invasive way to roll out that change (e.g. by having a long deprecation cycle).

这段话的最重要的信息是,尽管Swift 1到3,经常出现源代码不兼容的情况,Swift社区还是决定从Swift 3.x开始,尽可能保证之版本的Swift与Swift 3是源代码级别兼容的。然而,如果Swift社区觉得非常有必要的话,也还是会做出一些语言上的重大变化,只不过这一次社区将尽可能的用温和的方式。所以,大家从Swift 3开始,不需要过于担心升级Xcode以后大量代码编译不通过的问题了。

然而,Swift 3到4的二进制兼容是没有保证的,所以,Swift 3编译出来的的framework,那是不可以用到Swift 4的工程中的。

详情见Rewrite the Swift 3 “goals” and drop the “nongoals” section, now that it的内容,大家可以看到

* **Stable ABI**

部分被删除了,也就是说Swift 3和以后版本的Swift的二进制兼容是大概率不可能的了,你用Swift 3编译的静态库和动态库,那是不能用到Swift 4工程上的。

如果你要对外发布framework,那么,暂时不要用Swift编写framework。

如果你的代码以开源的方式对外发布,并且他人的集成方式也是源代码集成的话,你大可以可以安心了,不需担心要像之前那样,swift1一个分支,swift2一个分支,swift3一个分支,并且三个分支互不兼容。Swift 4的工程也可以使用Swift 3的代码,正如你可以在C++源文件中写C代码,在Objective-C++的mm文件中写C++代码。

继续谈谈Found an unexpected Mach-O header code: 0x72613c21

之前一篇文章中,我提到,我使用Xcode 8 Beta 2打开之前的工程,并将Swift 2.2的代码转换为Swift 3之后,再度编译工程,Xcode就会报错误Found an unexpected Mach-O header code: 0x72613c21

正好,Apple Developer Forums也有一个Thread提到了0x72613c21错误。
该文指出问题出在Fabrics/Crashlytics上,如果你遇到了Found an unexpected Mach-O header code: 0x72613c21错误,那可能是你集成了Fabric/Crashlytics,并且版本不够新,因为许多人反应,移除Crashlytics后就好了,或者升级Crashlytics到3.7.2也可以。

不过,我当时的做法既不是移除Crashlytics,也不是升级Crashlytics,而是直接废除了Swfit代码,完全用Objective-C重新实现了原来Swift代码所实现的功能。

该Thread的最后一篇回复是这么说的:

diego.trevisan
Aug 1, 2016 2:02 PM
(in response to davidfromsparks)
This is finally fixed in Beta 4!

diego.trevisan
2016年8月1日,下午 2:02
(回复:davidfromsparks)
这个问题终于在Beta 4中解决了!

看到8月1日的回复,我可以郑重告诉大家,如果你遇到了0x72613c21错误,并且还在用Xcode 8 Beta 4之前的版本,那请首先尝试升级Xcode 8到Beta 4或更新版本。

diego.trevisan
Jul 31, 2016 3:43 PM
(in response to jmac)
I have just another dependency which is Alamofire and it is build from their new swift2.3 branch. Anyway, removing Fabric/Crashlytics solves the problem. Just tried adding them again, and the error appears :/
我的工程中有一个依赖Alamofire,而Alamofire是以swift2.3的分支编译的。无论如何,我将Fabric/Crashlytics从工程中移除了,Xcode 8就不会报错,而我一旦将Fabric/Crashlytics添加到工程中,那么错误就会重新出现。

jmac
Jul 31, 2016 4:33 PM
(in response to diego.trevisan)
This would be an interesting test:

– Take the Fabric.framework and Crashlytics.framework that you have from the installer, and try linking them by hand with a sample project with no other dependencies. Based on what you’re seeing, I would expect that it would not be able to build with Xcode 8 and that you would see the same Mach-O error.

如果你通过Fabric的安装包集成了Fabric.framework 和 Crashlytics.framework,并尝试在一个样例工程中仅仅链接这两个framework,且不包含其他的依赖. 那么,根据你所看见的,我预计Xcode 8无法成功编译该工程并且会报告Mach-O错误。

Then, try installing Fabric/Crashlytics in your project via Cocoapods. I was installing them via pods when they worked for me (and the older versions that I had were from the installer). I had assumed that the difference was just the old ones vs the new ones, but it’s possible that something about the way crashlytics links them ends up being different from what the installer does.

接下来,你可以试着用Cocoapods而不是Fabric的安装包集成Fabric/Crashlytics。我用Cocoapods集成Fabric/Crashlytics,这不会导致Mach-O错误(当然,我用Cocoapods安装的是新版本的Fabric/Crashlytics,而之前用安装包安装的Fabric/Crashlytics的版本旧一些)。我估计导致错误的原因仅仅是Fabric/Crashlytics的版本的新旧,新版本的没问题,旧版本的有问题,但是,或许也还有别的可能性,或许用安装包安装的Fabric/Crashlytics在Xcode链接静态库的时候与Cocoapods的方式有着细微的差别,而这个差别导致了Mach-O错误。

Xcode 8 Beta 2转换原Swift 2.2代码到Swift 3.0后报错Found an unexpected Mach-O header code: 0x72613c21

今天试用了Xcode 8 Beta 2,尝试将之前Swift 2.2的代码转换为Swift 3.转换的过程一波三折,Swift 3改动十分大。

诸如dispatch_after这些在Swift 3中都改成了DispatchQueue.main.after这种形式,其次,很多Objc的对象也改了翻译的名字,如Swift 2.2中UIScreen.mainScreen()变成了UIScreen.main()。其实这些都还好,Xcode都可以帮我自动转换好。

还有一个令人非常烦恼的就是objc代码的导入机制的变化,如果你有很多旧的代码还没有用nonnull这些关键字修饰objc的属性和方法的话,你就会发现,原来在Swift 2.2中,导入的objc属性或者方法参数是默认unwrapped的,到了Swift 3中,变成了默认不unwrapped了,而这些代码你必须手动去修改。

然而,最令我感到意外的是,到了最后,Xcode在copy swift libraries的时候,又报了错误
error: Found an unexpected Mach-O header code: 0x72613c21

用这个错误信息去找,看上去最符合的信息就是https://github.com/CocoaPods/CocoaPods/issues/5598,该issue中提到要设置ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO,然而,我的所有的build settings都已经是ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO了,可是Xcode还是会报错Found an unexpected Mach-O header code: 0x72613c21。最后,我只能认为是Xcode 8 Beta 2的一个bug了。

Swift 从1到2,从2到3,都完全没有考虑向后兼容(Backward Compatibility),这只能说明Swift其实连个半成品都不算,各位学习Swift可以,千万不要用在实际的工程中,否则,坑死你。

Apple的Swift语言稳定了吗?

Apple推出了Swift语言,这引起了业内的广泛关注,然而,Swift仍然在迅速的演化之中,业内也不停地在讨论以下问题

  1. Swift会替代Objective C吗
  2. 我想开始iOS开发,该学习Obj-C还是Swift
  3. 现在改用Objective C开发还是用Swift
  4. 我们是否该从Objective C迁移到Swift

不过,如果你之前写了大量的Swift代码,那么,升级到Swift 1.2以后,你将遇到很多编译错误,例如以下的几个编译错误。

Method ‘load()’ defines Objective-C class method ‘load’, which is not permitted by Swift

Method ‘abc()’ with Objective-C selector ‘abc’ conflicts with getter for ‘abc’ from superclass ‘Abc’ with the same Objective-C selector

‘AnyObject’ is not convertible to ‘NSDictionary’; did you mean to use ‘as!’ to force downcast?

‘AnyObject’ is not convertible to ‘CALayer’; did you mean to use ‘as!’ to force downcast?

笔者在之前的项目中,也密切关注着Swift,不过,并没有急于大量将Swift应用到生产代码中,而是在几个小项目中试用了以下,这些代码到了1.2后,全部都编译出错,错误信息大多数都是上面几种情况。

虽然很快就找到了解决办法,可是,如果早几个月写了大量的Swift代码,现在却面临了1.2的改变而导致编译出错,全部都改过来也是挺烦的。

解决办法其实也不复杂。

其中第三和第四个其实就是as改为as!(即加上感叹号)

第二个,原来的readonly属性,我直接写了个 override func abc()->String!的方法,现在要改成

var abc:String!{ get{ return “abc” } }

即override属性,而不是override实现的方法了

第一个,不能重载load方法,目前只能把override的load的方法的swift code注释掉了,替代的办法没有。