分类目录归档:iOS

iOS 13的新Info.plist的key NSBluetoothAlwaysUsageDescription,相关应用需要及时升级

iOS 13在隐私方面有了新的变化,其中一个变化对于视频录制类应用会有较大影响。那就是Privacy – Bluetooth Always Usage Description(NSBluetoothAlwaysUsageDescription)。
如果视频类应用支持手持稳定器,如大疆Osmo Mobile,而目前手持稳定器与手机之间都是通过蓝牙连接的,那么,App的Info.plist中需要添加该Key,否则,App在尝试连接手持稳定器的时候,会crash。

Crash的错误信息如下
This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app’s Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data.

使用CoreMotion时遇到Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]

2019年8月1日更新:根据本人测试,在iOS 13 Developer Beta 5,iPhone XS Max上,本bug已经不再出现,App调用CoreMotion不会再出现Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState] 错误。


我近期以ProMovie为原型,开发一款iPhone的录像app,在开发的过程中,考虑到用户可能会锁定屏幕旋转,所以,我使用了CoreMotion判断设备当前的orientation,以此确定拍摄的视频的orientation。然而,在实际调试中,我发现,每次在我的iPhone XS Max iOS 12.3.1上运行,Xcode都会检测到以下错误。该错误不会导致应用崩溃,但是一直存在。最终,我只能像忽略warning一样忽略它,然而,与warning不一样之处在于,我可以想办法解决warning,却没办法解决此问题。

Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]
PID: 11696, TID: 3583674, Thread name: com.apple.CoreMotion.MotionThread, Queue name: com.apple.root.default-qos.overcommit, QoS: 0
Backtrace:
4 libobjc.A.dylib 0x000000022fc476f4 + 56
5 CoreMotion 0x00000002363c1d9c CoreMotion + 294300
6 CoreMotion 0x00000002363c22cc CoreMotion + 295628
7 CoreMotion 0x00000002363c21dc CoreMotion + 295388
8 CoreMotion 0x00000002363f001c CoreMotion + 483356
9 CoreMotion 0x00000002363f0060 CoreMotion + 483424
10 CoreFoundation 0x00000002309d627c + 28
11 CoreFoundation 0x00000002309d5b64 + 276
12 CoreFoundation 0x00000002309d0e58 + 2276
13 CoreFoundation 0x00000002309d0254 CFRunLoopRunSpecific + 452
14 CoreFoundation 0x00000002309d0f88 CFRunLoopRun + 84
15 CoreMotion 0x00000002363ef9f4 CoreMotion + 481780
16 libclang_rt.asan_ios_dynamic.dylib 0x00000001053e1ef0 _ZN6__asan10AsanThread11ThreadStartEyPN11__sanitizer16atomic_uintptr_tE + 192
17 libsystem_pthread.dylib 0x000000023064e908 + 132
18 libsystem_pthread.dylib 0x000000023064e864 _pthread_start + 48
19 libsystem_pthread.dylib 0x0000000230656dcc thread_start + 4
2019-07-22 16:56:00.955489+0800 VideoCamera[11696:3583674] [reports] Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]
PID: 11696, TID: 3583674, Thread name: com.apple.CoreMotion.MotionThread, Queue name: com.apple.root.default-qos.overcommit, QoS: 0
Backtrace:
4 libobjc.A.dylib 0x000000022fc476f4 + 56
5 CoreMotion 0x00000002363c1d9c CoreMotion + 294300
6 CoreMotion 0x00000002363c22cc CoreMotion + 295628
7 CoreMotion 0x00000002363c21dc CoreMotion + 295388
8 CoreMotion 0x00000002363f001c CoreMotion + 483356
9 CoreMotion 0x00000002363f0060 CoreMotion + 483424
10 CoreFoundation 0x00000002309d627c + 28
11 CoreFoundation 0x00000002309d5b64 + 276
12 CoreFoundation 0x00000002309d0e58 + 2276
13 CoreFoundation 0x00000002309d0254 CFRunLoopRunSpecific + 452
14 CoreFoundation 0x00000002309d0f88 CFRunLoopRun + 84
15 CoreMotion 0x00000002363ef9f4 CoreMotion + 481780
16 libclang_rt.asan_ios_dynamic.dylib 0x00000001053e1ef0 _ZN6__asan10AsanThread11ThreadStartEyPN11__sanitizer16atomic_uintptr_tE + 192
17 libsystem_pthread.dylib 0x000000023064e908 + 132
18 libsystem_pthread.dylib 0x000000023064e864 _pthread_start + 48
19 libsystem_pthread.dylib 0x0000000230656dcc thread_start + 4

然而,令我感到困惑的是,Stack Trace给出的信息表明这应该是CoreMotion自己的问题。

我通过搜索,发现Stackoverflow上有相应的问题和回答
https://stackoverflow.com/questions/54607856/main-thread-checker-warning-with-coremotion-only-appearing-on-2018-model-iphone
根据该问答,2018款的iPhone都可以复现此问题。这样看来,这是一个iPhone的bug了。

我于是拿来了我的iPhone 6s进行测试,果然,在我的iPhone 6s iOS 12.3.1上,我的app并不会复现此问题。

然而,经过搜寻,我又发现了如下的bug report
https://openradar.appspot.com/46210367

有人报告,在使用UIInterpolatingMotionEffect时,也会出现UI API called on a background thread: -[UIApplication applicationState] Thread name: com.apple.CoreMotion.MotionThread 错误,且该错误仅在iPhone XS上出现,在iPhone X,6s和6s Plus上则不能复现此错误。因此,该report认为,bug与新的硬件相关,具体什么原因,未知。需要注意的是,该bug report提到的iOS版本为12.1,而截止到本文写作之时,iOS已经升级到了12.3.1,很快就要一年了,该bug仍然存在。

A new instance of CMMotionManager results in a Thread Checker warning
Originator: futuretap
Number: rdar://46210367 Date Originated: 22-Nov-2018 10:46 AM
Status: Duplicate/31658500/Closed Resolved:
Product: iOS + SDK Product Version: 12.1
Classification: Serious Bug Reproducible: Always

Summary:
This is a duplicate of radar #45003816. We encounter the same issue when using UIInterpolatingMotionEffect. The Main Thread Checker kicks in on an iPhone XS@12.1.
iPhone X@12.1.1, iPhone 6s@11.4.1, iPhone 6s Plus@11.4.1 all work fine so it seems to depend on the new hardware.

Creating a new instance of CMMotionManager always results in a Thread Checker exception

Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]
PID: 9123, TID: 1958556, Thread name: com.apple.CoreMotion.MotionThread, Queue name: com.apple.root.default-qos.overcommit, QoS: 0
Backtrace:
4 libobjc.A.dylib 0x00000002079d7894 + 56
5 CoreMotion 0x000000020e2387a4 CoreMotion + 305060
6 CoreMotion 0x000000020e238cd8 CoreMotion + 306392
7 CoreMotion 0x000000020e238be8 CoreMotion + 306152
8 CoreMotion 0x000000020e26a3cc CoreMotion + 508876
9 CoreMotion 0x000000020e26a42c CoreMotion + 508972
10 CoreFoundation 0x0000000208770888 + 28
11 CoreFoundation 0x000000020877016c + 276
12 CoreFoundation 0x000000020876af54 + 1016
13 CoreFoundation 0x000000020876a844 CFRunLoopRunSpecific + 452
14 CoreFoundation 0x000000020876b5a8 CFRunLoopRun + 84
15 CoreMotion 0x000000020e269d64 CoreMotion + 507236
16 libsystem_pthread.dylib 0x00000002083e5a04 + 132
17 libsystem_pthread.dylib 0x00000002083e5960 _pthread_start + 52
18 libsystem_pthread.dylib 0x00000002083eddf4 thread_start + 4
2018-10-04 12:09:35.737994+0200 test[9123:1958556] [reports] Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]
PID: 9123, TID: 1958556, Thread name: com.apple.CoreMotion.MotionThread, Queue name: com.apple.root.default-qos.overcommit, QoS: 0

Comments
Happening for me as well, lot of things getting impacted, and yeah only happens on XS, XS Max, and XR

By jchaudhry at May 6, 2019, 11:36 p.m. (reply…)
This issue still happens. Should I file yet another bug report with Apple?

By aruslan at April 2, 2019, 6:34 p.m. (reply…)
Any update on this issue, seems like this is massively impacting a lot of projects

By xuehao.hu at March 29, 2019, 6:11 a.m. (reply…)

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方法,运行效率更高。

解决问题:iOS模拟器无法联网了

今天在Xcode上用模拟器进行调试的时候,突然发现一个奇怪的问题,如果我用了iOS 8版本的模拟器,无论是我的应用还是Simulator中的Safari,都无法连接互联网。Safari报告的错误是:“Safari cannot open the page because the network connection was lost”。我如果用iOS 11的模拟器,就没有这个问题。

我通过百度,Bing等搜索引擎进行了查找,找到了如下信息:
Can’t use HTTP in iOS 8 simulator https://stackoverflow.com/questions/25654679/cant-use-http-in-ios-8-simulator

ios模拟器连不上网 https://www.2cto.com/os/201612/576530.html

xcode 模拟器无法连接网络 http://www.cocoachina.com/bbs/read.php?tid-226644-page-e-fpage-96.html

结果,以上的方案没有一个适合我的情况。

最后,我想起来,我安装了Avast Security,并且启用了Web Shield,我关闭了Web Shield的话,iOS 8模拟器就可以联网了。
如果你也遇到了类似的问题,如果常规的方法不能解决的话,不妨检查一下你是不是也安装了类似的安全软件。

NSDateFormatter性能优化

近期在某个项目上,遇到了一个NSDateFormatter的性能问题。

不知何故,app中需要把将近一千个字符串转换为NSDate类型,运行后发现,转换居然要600ms以上,实在是不可忍受。最开始,我发现代码是每次进行转换都新创建一个NSDateFormatter对象,既然如此,我就进行了第一步优化,创建并缓存了全局的NSDateFormatter对象。可是,再次运行后发现,这个优化居然没有效果!!!怎么回事,不是苹果自己的文档说的“Cache Formatters for Efficiency”吗?

我用Instrument对应用进行了Profile,发现,创建一个NSDateFormatter并不消耗太长时间,反而是stringFromDate和dateFromString这两个方法耗时间,相比之下,创建NSDateFormatter的时间完全可以忽略不计。

最后,发现NSDateFormatter性能实在是太差,只能用sqlite了。

        sqlite3_stmt *statement = NULL;
        sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
        
        sqlite3_bind_text(statement, 1, [str UTF8String], -1, SQLITE_STATIC);
        sqlite3_step(statement);
        sqlite3_int64 interval = sqlite3_column_int64(statement, 0);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:interval];

strftime可以将yyyy-MM-dd HH:mm:ss这样的字符串转换为timestamp,然后再根据timestamp创建NSDate对象,这样比直接dateFromString要快很多。

同样,stringFromDate性能也很差,我最后直接用

calendar = [NSCalendar calendarWithIdentifier:NSGregorianCalendar];

获取到NSDateComponents,然后用

[NSString stringWithFormat:@"%4ld-%02ld-%02ld", components.year, components.month, components.day];

这样的代码获取字符串的日期和时间。

经过Instrument的Profiling,我发现calendarWithIdentifier:这个方法才是真的消耗时间,所以我缓存了NSCalendar对象。

结论,优化NSDateFormatter的结果就是别用NSDateFormatter。

resource fork, Finder information, or similar detritus not allowed

今天,我准备更新我的一个app,这个app很久以前上架的,当时还是iPhone 32位 CPU的时候,所以有必要更新一次。

然而,在编译过程中,我遇到了以下错误:

resource fork, Finder information, or similar detritus not allowed

经过一番搜索,找到了解决方案。

http://stackoverflow.com/questions/39652867/code-sign-error-in-macos-sierra-xcode-8-resource-fork-finder-information-or

https://developer.apple.com/library/content/qa/qa1940/_index.html

原来,MacOS的文件有三个fork,data fork, resource fork和Finder info。Data fork存储了文件的内容;resource fork保存了一些扩展信息,如什么应用创建了这个文件,又例如上次你打开这个txt文件的时候,正在显示的是第几行,等等; Finder Info则保存了文件所有者,创建者等信息。

从iOS 10和macOS Sierra开始,从安全考虑,app bundle中的文件将不能包含resource for和Finder info了。所以,我们必须去掉这两个信息,才能成功进行代码签名。具体如何去掉这两块信息,大家可以参考下面的说明。

40
Code signing fails with error 'resource fork, Finder information, or similar detritus not allowed'

Q:  When I build my app, code signing fails with the error "resource fork, Finder information, or similar detritus not allowed." What does this mean and what should I do about it?

A: This is a security hardening change that was introduced with iOS 10, macOS Sierra, watchOS 3, and tvOS 10.

Code signing no longer allows any file in an app bundle to have an extended attribute containing a resource fork or Finder info.

To see which files are causing this error, run this command in Terminal:

$ xattr -lr 

replacing  with the path to your actual app bundle.

Here's an example of this command in action:

$ xattr -lr Foo.app
/Applications/Foo.app: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  |................|
You can also remove all extended attributes from your app bundle with the xattr command:

$ xattr -cr 

Note that browsing files within a bundle with Finder's Show Package Contents command can cause Finder info to be added to those files. Otherwise, audit your build process to see where the extended attributes are being added

更新到cocoapods 1.2.1了

今天准备更新到最新的cocoapods。我在mac上执行了遇到了一系列错误

$ sudo gem install cocoapods
ERROR: While executing gem ... (Gem::DependencyError)
Unable to resolve dependencies: cocoapods requires cocoapods-core (= 1.2.1), claide (< 2.0, >= 1.0.1), cocoapods-downloader (< 2.0, >= 1.1.3), cocoapods-trunk (< 2.0, >= 1.2.0), molinillo (~> 0.5.7), xcodeproj (< 2.0, >= 1.4.4), colored2 (~> 3.1), fourflusher (~> 2.0.1), ruby-macho (~> 1.1)

随后,我只能逐个执行gem install安装上述错误信息提供的依赖,结果,这样就安装成功了。似乎rubygems没有成功解决依赖,让我手动解决了????

$ sudo gem install cocoapods-core
Fetching: cocoapods-core-1.2.1.gem (100%)
Successfully installed cocoapods-core-1.2.1
Parsing documentation for cocoapods-core-1.2.1
Installing ri documentation for cocoapods-core-1.2.1
Done installing documentation for cocoapods-core after 1 seconds
1 gem installed


$ sudo gem install claide
Fetching: claide-1.0.1.gem (100%)
Successfully installed claide-1.0.1
Parsing documentation for claide-1.0.1
Installing ri documentation for claide-1.0.1
Done installing documentation for claide after 0 seconds
1 gem installed


$ sudo gem install cocoapods-downloader
Fetching: cocoapods-downloader-1.1.3.gem (100%)
Successfully installed cocoapods-downloader-1.1.3
Parsing documentation for cocoapods-downloader-1.1.3
Installing ri documentation for cocoapods-downloader-1.1.3
Done installing documentation for cocoapods-downloader after 0 seconds
1 gem installed

$ sudo gem install cocoapods-trunk
Fetching: cocoapods-trunk-1.2.0.gem (100%)
Successfully installed cocoapods-trunk-1.2.0
Parsing documentation for cocoapods-trunk-1.2.0
Installing ri documentation for cocoapods-trunk-1.2.0
Done installing documentation for cocoapods-trunk after 0 seconds
1 gem installed

$ sudo gem install molinillo
Fetching: molinillo-0.5.7.gem (100%)
Successfully installed molinillo-0.5.7
Parsing documentation for molinillo-0.5.7
Installing ri documentation for molinillo-0.5.7
Done installing documentation for molinillo after 0 seconds
1 gem installed

$ sudo gem install xcodeproj
ERROR: While executing gem ... (Gem::DependencyError)
Unable to resolve dependencies: xcodeproj requires colored2 (~> 3.1), nanaimo (~> 0.2.3)

$ sudo gem install colored2
Fetching: colored2-3.1.2.gem (100%)
Successfully installed colored2-3.1.2
Parsing documentation for colored2-3.1.2
Installing ri documentation for colored2-3.1.2
Done installing documentation for colored2 after 0 seconds
1 gem installed
mbp2:Caches jimmy$ sudo gem install nanaimo
Fetching: nanaimo-0.2.3.gem (100%)
Successfully installed nanaimo-0.2.3
Parsing documentation for nanaimo-0.2.3
Installing ri documentation for nanaimo-0.2.3
Done installing documentation for nanaimo after 0 seconds
1 gem installed

$ sudo gem install molinillo
Successfully installed molinillo-0.5.7
Parsing documentation for molinillo-0.5.7
Done installing documentation for molinillo after 0 seconds
1 gem installed

$ sudo gem install fourflusher
Fetching: fourflusher-2.0.1.gem (100%)
Successfully installed fourflusher-2.0.1
Parsing documentation for fourflusher-2.0.1
Installing ri documentation for fourflusher-2.0.1
Done installing documentation for fourflusher after 0 seconds
1 gem installed
mbp2:Caches jimmy$ sudo gem install ruby-macho
Fetching: ruby-macho-1.1.0.gem (100%)
Successfully installed ruby-macho-1.1.0
Parsing documentation for ruby-macho-1.1.0
Installing ri documentation for ruby-macho-1.1.0
Done installing documentation for ruby-macho after 0 seconds
1 gem installed

iOS/Android的WebView中用file input支持拍照或选择相册的照片

如果我们的一个移动端的网页需要让用户上传一张照片,那么,通常而言,我们可以写了以下一段HTML代码

<input type='file' />

那么Mobile Web界面将会显示下面的一个控件,通过该控件,用户可以拍照或者选择手机中的文件而上传。

在iOS下,Mobile Safari会在你点击上面的控件之后弹出如下界面
Screen Shot 2017-03-12 at 下午6.44.31

在Android系统下,弹出的界面根据具体的Android版本(不同的厂商定制版本、不同的Android系统版本)不同而略有不同,不过,通常也会提供相机,相册等选项。下图为小米手机的例子。
IMG_20170312_184934

然而,如果你在原生的应用中内嵌了一个UIWebView/WKWebView(iOS)或者WebView(Android),你就会发现,原来在Mobile Safari和Chrome中可以正常运行的代码很有可能不能正常工作了。

在iOS系统下,你会发现,如果UIWebView所在的View Controller是通过presentViewController展示的,那么,你会发现,用户点击 后,你的iOS应用会出现一些奇怪的问题,如应用崩溃,如你在相册选择了照片之后,UIWebView所在的View Controller不见了,等等。

最后,我的解决方法是,不通过presentViewController展示UIWebView所在的View Controller,而是通过UINavigationController的pushViewController去展示View Controller。

而对于Android系统,应用内嵌的WebView本来就不支持使用文件上传功能,所以,<input type=’file’ />在Mobile Chrome上有效,到了应用内嵌的WebView中就无效了。

在Android上,我的解决方法是这样的,通过WebView的addJavascriptInterface注入一个camera对象,让WebView中的js代码通过camera对象从而唤起相机或者相册。

webView.addJavascriptInterface(new DemoJavaScriptInterface(), “demo”);

Javascript代码

window.demo.camera();

假设DemoJavaScriptInterface的是位于WebViewActivity里面的一个内部类,且WebViewActivity有一个私有变量String mCameraPhotoPath,那么,DemoJavaScriptInterface代码如下

 final class DemoJavaScriptInterface {
        DemoJavaScriptInterface() {
        }

        @JavascriptInterface
        public void camera() {

                Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
                    // Create the File where the photo should go
                    File photoFile = null;
                    try {
                        photoFile = createImageFile();
                        takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
                    } catch (Exception ex) {
                        // Error occurred while creating the File
                        Log.e("WebViewSetting", "Unable to create Image File", ex);
                    }

                    // Continue only if the File was successfully created
                    if (photoFile != null) {
                        mCameraPhotoPath = photoFile.getAbsolutePath();
                        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                                Uri.fromFile(photoFile));
                    } else {
                        takePictureIntent = null;
                    }
                }

                Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
                contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
                contentSelectionIntent.setType("image/*");

                Intent[] intentArray;
                if (takePictureIntent != null) {
                    intentArray = new Intent[]{takePictureIntent};
                } else {
                    intentArray = new Intent[0];
                }

                chooserIntent = new Intent(Intent.ACTION_CHOOSER);
                chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
                chooserIntent.putExtra(Intent.EXTRA_TITLE, "拍照或者选择图片");
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

                Activity mContext = WebViewActivity.this;
                if (Build.VERSION.SDK_INT >= 23) {
                    int checkCallPhonePermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA);
                    if (checkCallPhonePermission != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(mContext, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_ASK_CAMERA);
                        return;
                    }
                }

                WebViewActivity.this.startActivityForResult(chooserIntent, 101);

            }

    }

WebViewActivity中的onActivityResult方法代码如下

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
                case 101:
                    if(data == null){
                        String imageurl = mCameraPhotoPath;
                        //相机拍好的照片就保存在路径imageurl中
                    }
                    else{
                        Uri uri = data.getData();
                        //通过uri获取照片数据
                    }

                    break;

                default:
                    return;
            }
        }

    }

openbakery gradle-xcodePlugin在keychainCreate阶段报错

我在iOS的项目中使用了gradle-xcodePlugin进行打包工作,然而,最近gradle-xcodePlugin一直报如下错误。


:archive
:keychainCreate FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':keychainCreate'.
> Command failed to run (exit code 1): 'security set-key-partition-list -S apple: -k -D -t private'

我将

security set-key-partition-list -S apple: -k -D -t private

在终端中执行的话,会遇到如下错误信息。

security: SecKeychainItemSetAccessWithPassword: The user name or passphrase you entered is not correct.

我在https://github.com/openbakery/gradle-xcodePlugin/issues/316 看到,已经有人报告了一个类似的issue。
原来gradle-xcodePlugin的作者为了解决issue 316而在0.14.5中添加了’security set-key-partition-list -S apple: -k -D -t private’命令。正是这个命令导致了我的打包失败。

最终,我改用了0.14.4版本的gradle-xcodePlugin,就可以正常打包了。

说说ITMS-90682: can’t contain 16-bit or P3 assets if the app supports iOS 8 or earlier

昨日,我在工作中听到我部门打包好的IPA,到了测试的时候,一旦测试的手机是iOS 8或者9.2的手机,应用总是会随机崩溃。最后,同事们找到了原因,具体原因见
http://stackoverflow.com/questions/39404285/xcode-8-build-crash-on-ios-9-2-and-below

When I build my app with Xcode 8 GM Seed and run it on an iOS 9.2 below device OR simulator, I get strange EXC_BAD_ACCESS crashes during app startup or a few seconds after the app launched. The crash always happens in a different spot (adding a subview, [UIImage imageNamed:], app delegate’s main method etc). I don’t get those crashes when I run it on iOS 9.3+ or 10 and I don’t get them when I build with Xcode 7 and run on iOS 9.2 and below. Has anyone else experiences something similar? Is this a known issue with Xcode 8?

当我用Xcode 8 GM Seed打包应用并在iOS 9.2或者更低版本的设备或模拟器运行的时候,我总是会在应用启动时或启动后的数秒之内遇到EXC_BAD_ACCESS,而崩溃的地方每次都不一样,如addSubView,[UIImage imageNamed:]甚至main方法。如果我在9.3或者10上运行,或者我用Xcode 7执行编译工作,然后运行在9.2以及以下的设备上,这些错误都不会发生。有没有人遇到类似的事情呢,这是不是Xcode 8的一个问题?

而解决的方法在这里讲了https://forums.developer.apple.com/thread/60919?start=0&tstart=0。简单说,Xcode认为你的图片资源中包含了16位或者P3的资源,所以打包的时候会进行相应的处理,但是,只有9.3或者更高版本才支持这样的资源,所以到了9.2就会崩溃。

此时,我想说说什么是P3资源。P3指的是DCI-P3或者说DCI/P3,一种数字电影投影所用的色域标准,它能比sRGB显示更多的颜色。苹果在推出5k显示屏的new iMac的时候(2015年10月),引入了DCI-P3,随后9.7寸的iPad Pro(2016年3月推出,推出时iOS 9.3系统)、iPhone 7(2016年9月推出,iOS 10系统)和2016的MacBook Pro都使用了DCI-P3色域。即苹果公司的iOS 9.3系统支持DCI-P3色域,而iPad Pro作为第一个支持DCI-P3的iPad,使用的是iOS 9.3。这也就解释了为什么我们之前打包的IPA到了9.2的设备上就会不正常。

那么,如何判断一个图片是不是P3的图片呢,其实很简单,就是要看这个图片的Color Profile是什么,通常来说,一个图片应该没有内嵌Color Profile或者内嵌sRGB的Color Profile。下面一个例子,我进行了两张图片的对照,左边为P3的png,右边为普通的png。P3和非P3图片对照

不难发现,左侧的图片有内嵌的Color Profile:Display,而右侧的常规的图片没有内嵌的Profile。那问题来了,为什么Color Profile: Display是P3资源呢?Display难道等于DCI-P3吗?

这里需要说明的是,Display指的是制作图片的人所使用的电脑的显示器所使用的Color Profile,具体这个Profile是什么,那要看制作者的电脑上的Display对应的Color Profile是什么,如果制作者使用的是5k的iMac,那么Display对应的Profile就是P3了。
下图为笔者的MacBook Pro的Photoshop新建一个图像的对话框。
display_and_display_p3
我们看到,默认的是Display,即用户当前显示器所使用的Color Profile,此外,还有许多Color Profile可以选择,如Display P3,这就是DCI-P3色域的Color Profile。如果你用的是5k的new iMac,那么Display就等于Display P3,如果你用的是别的Mac,比如笔者的2013的MacBook Pro,那么Display就等于sRGB。

下图为2016的MacBook Pro的技术规格,请大家注意“广色域(P3)”这几个字。这就是前面提到的P3 assets中P3的含义。
2016 MacBook Pro的显示屏采用了广色域P3

解决方法如下,摘录自https://forums.developer.apple.com/thread/60919?start=0&tstart=0,我就不翻译了。当然,最好是设计师在设计资源的时候,如果使用的是5k的iMac或者2016年新推出的MacBook Pro,那么,不要选择默认的Display选项,要选择sRGB,这样图片资源就可以用于支持iOS 8和iOS 9.2版本的工程了。

You can find 16-bit or P3 assets by running “assetutil” on the asset catalog named in the error message from iTunes Connect. The following steps outline the process:
1. Create an Inspectable .ipa file. In the Xcode Organizer (Xcode->Window->Organizer), select an archive to inspect, click “Export…”, and choose “Export for Enterprise or Ad-Hoc Deployment”. This will create a local copy of the .ipa file for your app.
2. Locate that .ipa file and change its the extension to .zip.
3. Expand the .zip file. This will produce a Payload folder containing your .app bundle.
4. Open a terminal and change the working directory to the top level of your .app bundle
cd path/to/Payload/your.app

5. Use the find tool to locate Assets.car files in your .app bundle as shown below:
find . -name ‘Assets.car’

6. Use the assetutil tool to find any 16-bit or P3 assets, in each Assets.car your application has as shown below. :
sudo xcrun –sdk iphoneos assetutil –info /path/to/a/Assets.car > /tmp/Assets.json

7. Examine the resulting /tmp/Assets.json and look for any contents containing “DisplayGamut”: “P3” and its associated “Name”. This will be the name of your imageset containing one or more 16-bit or P3 assets.

8. Replace those assets with 8-bit / sRGB assets, then rebuild your app.

Update: If your Deployment Target is set to either 8.3 or 8.4 and you have an asset catalog then you will receive this same error message, even if you do not actually have 16-bit or P3 assets. In this case you will either need to lower your Deployment Target to 8.2, or move it up to 9.x.