在Swift中捕获objc exception的方法

Swift虽然有比ObjC更加成熟的异常处理系统,但Swift本身是无法捕捉ObjC的异常的。理论上苹果在提供Swift接口时会将一些异常转化成Swift的异常,但就目前来讲(Swift3.1),objc runtime部分的异常并没有被转化成Swift异常。今天我就碰到了一次KVO removeObserver不当,抛出ObjC异常导致程序崩溃的情况。

按照NShipster的说法,安全的移除KVO observer的方法是:

然而苹果并没有对removeObserver这个方法加入Swift异常,导致单纯使用Swift无法捕捉该异常。

StackOverflow中的这篇回答给出了该问题的解决方案。原理是通过Objective C封装一个函数,函数签名中带有 error:(__autoreleasing NSError **)error; ,Swift调用该函数时,能自动将其转换为Swift Exceptions。

完整代码:

Swift中的调用:

 

Objective-C小技巧之——如何将自定义类的对象作为字典的Key

Objective-C里面,要把自定义类产生的对象作为字典的Key不想Java那样自然而然,而是要做很多事情。

前年写过一篇文章,提到了如何把自定义类的对象变成字典的key。当时为了搞明白这个,连OPENSTEP的源码都看了。这种方法虽然正统,但是非常麻烦,不仅要实现NSCopying,而且要把equal和hash都写了,有点悲剧。但实际上有一个小技巧可以在不实现这些方法的情况下,将对象作为key,那就是用NSValue。

参考文章:

http://stackoverflow.com/questions/11532306/using-an-object-as-key-for-nsdictionary
http://nshipster.com/nsvalue/

示例代码:

使用Python读取IPA中的软件信息

最近公司需要对所有的iOS工程进行持续集成,软件的编译、打包、内测版的ipa发布都需要自动化。在做ipa自动化发布时,需要拿到一个打包好的ipa之后提取出里面相关的信息,比如显示名称、版本号、bundle identifier等等。

其实要做到这一点是非常容易的,前提是你要对ipa包的结构有所了解。

ipa文件结构

首先,一个ipa其实就是一个zip文件改了后缀名。如果你把ipa的后缀改回.zip,那么你就能通过各种解压软件直接解压了。

解压后的目录结构是:

其中,Info.plist就是软件信息的所在。

但是别高兴得太早,这里的Info.plist并不是纯文本文件,而是一种被称为Binary Plist的东西。之前在做iOS开发时,工程里面用的plist几乎都是XML形式的纯文本文件,所以一开始我理所当然地认为这个也是纯文本文件,结果在这里卡了半天。在命令行运行man plist就能看到下面这段话:

The property list programming interface allows you to convert hierarchically structured combinations of these basic types to and from two formats: standard XML and an optimized, opaque binary format.

所以不能看到.plist结尾的文件就当文本文件处理。

Python中相应的library

在知道ipa的结构之后,就能知道需要用哪些Python library了,第一个是zipfile,这个库能够处理zip类型的文件,并且可以在不解压的情况下读取里面某个文件的内容。另外一个是plistlib。但是需要注意的是,在Python 3.4之前,这个库是不支持 Binary Plist 的解析的。所以如果使用 Python 2.7 的话,需要使用三方库biplist

下面使用 Python 3.4 为例进行解析。之所以选择 Python 3.x 是因为 2.x 版本在处理 Unicode 和非 Unicode 混用时非常麻烦。而 Python 3.x 起,所有的 string 都使用 Unicode 编码了,就跟 ObjC 中一样。

首先使用zipfile将ipa文件打开,zipfile中,namelist()方法能够列出里面包含的所有文件路径,并返回一个list。根据ipa的结构,我们要找的Info.plistPayload/软件名字.app/Info.plist,所以这里使用一个正则表达式找到这个文件路径。

ZipFile.read()这个方法,能够在不解压zip文件的情况下读取里面的内容,在Python 3.4中返回的是bytes类型,再使用plistlib.loads()载入。其中,loads()是 Python 3.4 才加入的新API,它接收一个bytes对象,将其解析成相应的Python对象。

这样,这个plist文件就变成了一个dict,从而可以取到里面的内容。

如果使用 Python 2.7, 则可以用biplist代替,这个库使用的是跟 Python 2.7 中plistlib相同的API。

如果不使用 Python,或者不想用这些类库,则可以通过一些外部程序来解决Binary Plist的读取问题。比如使用/usr/libexec/PlistBuddy或者是plutil,这些都是 OS X 自带的工具,通过它们,你可以读取Plist中的指定项,或者直接把plist变成JSON文件。

[译] Xcode 6的“矢量图支持”是如何工作的

翻译自这个答案

矢量图在Xcode 6中如何使用

  1. 以一倍图(@1x)大小将图片保存成PDF格式的矢量图
  2. Images.xcassets文件中新建一个Image Set
  3. attributes inspector中,将Type设为Vectors
  4. 将pdf文件拖拽到All, Universal
  5. 这样,你就可以通过名称(指images.xcassets对此图片起的名称)来指向这个图片,跟使用png图片一样。

矢量图在Xcode 6是如何工作的

“矢量图支持”在Xcode 6中可能跟大家想象的有所出入,因为很多人想到矢量图的时候,他们想到的是那些可以被扩大,缩小,但是还能显示得完好的图片。然而,Xcode 6对于iOS的矢量图支持并不完全,所以它的做法有点不同。

为了消除这种疑惑,在此说明矢量图系统的工作流程如下:

  • 它只是一个转换系统,它在 构建期 通过你的png文件生成@1x.png, @2x.png, @3x.png

比如,假定你有44×44大小的foo.pdf矢量图,在 构建期 ,它会生成如下的文件:

  1. foo@1x.png 44 x 44
  2. foo@2x.png 88 x 88
  3. foo@3x.png 132 x 132

这意味着:

  • 你不能更改图片的大小(指的是把一个44×44的图片塞进100×100的ImageView里)。它只有在44×44的大小中才看起来对。原因是(iOS)并没有实现矢量图的完全支持。这些矢量图的作用只是让你保存image assets的时候帮你节省时间。如果你已经用了某些工具,比如一个Photoshop脚本,这些pdf文件的作用只是对未来可能出现的情况的适配(比如如果苹果要求对iOS 9进行@4x图片的支持的时候),以及减少文件的管理。
  • 你必须将所有的矢量图保存为@1x大小的PDF格式。这样做才能保证UIImageView有正确的固有大小(intrinsic content size)

苹果这么做的原因是:

  • 能对老版本的iOS向下支持。
  • 运行时缩放矢量图是一个计算密集型的工作,但是通过上面这种方式,就不会对性能造成任何影响。

MHGJavascriptBridge简介

用途

在iOS开发中,我们经常会碰到这样的需求:在UIWebView中的一个链接,点了之后不是进下一个网页,而是进下一个UIViewController,或者让ObjC代码做点事情。这在资讯类的应用中很常见,比如网易新闻、腾讯新闻,以及我们公司的东方财富通中的资讯。

而在旧的iOS版本中,系统不提供在Javascript直接调用ObjC的方法。只能通过变换location等发起网络请求的方式,使得UIWebViewDelegate中的- (BOOL)webView:shouldStartLoadWithRequest:navigationType:感知到,进而做ObjC的处理。

然而这样做比较不优雅,所有的事情都围绕在URL请求上面,而不是方法调用上面,看上去不优雅。MHGJavascriptBridge的用意便是将URL请求等等封装起来,让Javascript和ObjC代码注重于方法调用本身上来。

Github地址:https://github.com/hikui/MHGJavascriptBridge

使用方法

MHGJavascriptBridge由3个文件组成,MHGJavascriptBridge.h, MHGJavascriptBridge.m, MHGJavascriptBridge.js。将这三个文件加入Xcode工程中。注意,MHGJavascriptBridge.js必须加入到资源文件中(在”Building phases” -> “Copy bundle resources”中出现),Xcode默认会将.js文件加入到Compile Sources里面去,这是错误的。

Objective C设置

首先,我们需要初始化一个bridge,这通常是在一个UIViewController中进行的。这里假设在UIViewController中对bridge进行初始化。在初始化中,需要设定bridge的webView属性:

在MHGJavascriptBridge中,所有能被Javascript调用的Objective C方法将以block的形式呈现。首先我们需要定义一些blocks,然后对每一个block起名。

MHGJavascriptBridge的原理是构造特定的URL,并且用UIWebViewDelegate中的- (BOOL)webView:shouldStartLoadWithRequest:navigationType:拦截这个URL。所以在这个delegate方法中,我们需要加入拦截语句:

其中,interceptRequest:方法会返回一个BOOL,如果拦截成功,则返回YES

这样,Objective C部分就设置完成了。其中需要注意的是,block被调用时,会传入一个dict,这是Javascript部分代码调Objective C代码时所传的参数。

Javascript设置

Javascript部分设置比较简单,最基本的设置是要保证UIWebView中的HTML引入了MHGJavascriptBridge.js

Javascript调用Objective C代码

一旦设置完成之后,Javascript和Objective C就能互相调用了。代码如下:

这时,点击button1时,就能触发Objective C的代码了。MHGJavascriptBridge.callNativeBlock有两个参数,第一个参数是在Objective C中注册的block名字,第二个参数是传给block里面的dict的额外信息。其中第二个参数必须是一个字典(或者说是一个Javascript Object),或者什么都不传。

Objective C调用Javascript代码

方法和上述类似:

其中第一个参数是Javascript函数名。如果你在HTML中定义了function xxx(){}或者var xxx = function(){}的话,就能被调用。第二个参数是一个数组,传的是Javascript函数要用的参数列。

局限性

  • Javascript调用Objective C时,所有的调用都是异步的,暂时无法实现同步调用。
  • Javascript调用Objective C时,所传参数受URL长度限制而限制。

UIViewController解耦尝试

当我们使用UIViewController时,从一个ViewController跳到另外一个ViewController,最简单的代码(不用storyboard的情况下)就是alloc一个实例,然后用navigation controller去push它。比如

而这样一来,这个ViewController必须知道另外一个ViewController的存在,而且需要import它的头文件。这样一来,两个ViewController就紧密耦合了。

在一般的程序里面,这么做没有任何问题。但是我们公司的两个主打产品有一个特殊需求就是:两个产品共用部分界面,而这些共用界面中的一些按钮,点击之后,在不同的程序里需要push出不同的界面(界面初始化需要的参数是一样的,但就是完全不同的界面)。而这两个产品又是由不同的小组去完成的,他们不共享同一个工程。

这时,我们想到的是将这些共用界面提取出来做成framework。那么问题就出现了:我如果在framework里面的view controller新建一个下一级view controller,就需要知道下一级view controller的头文件,下一级view controller又实际存在于每一个主工程里面。这样就出现了循环依赖的情况,这样做出来的framework即使能编译通过也没法维护。

好在ObjC是一个动态语言,实际上我们根本不需要知道一个类的信息就能初始化它。使用的方法就是NSClassFromString()函数,此时你只要知道class名字就行,如果它返回一个Class不为nil,就可以实例化它,并且用KVC注入需要的property。所以稍加改进的代码如下:

虽然这个能解耦,但是每次写那么冗余的代码总归不易维护。所以我尝试把这些东西抽象出来,以配置文件的形式,在不同工程里配置不同的界面。灵感来自于前几年在写Java的时候用的Spring。最后做了一个叫做EMViewControllerManager的东西。

EMControllerManager的配置文件的基本结构如下:

其中Test1Test2被称为view controller名称(昵称)。ClassName指定了view controller的真实类名,而Dependencies可以做一点基础的DI功能。

然后在AppDelegate里面载入配置文件:

而在view controller里面,可以这么来初始化一个view controller对象:

其中Named:是view controller的昵称,withPropertyValues:是初始化之后马上注入的property。

不同的工程只要改配置文件,而不需要改具体的代码,就能达到差异化的效果。

具体用法请阅读EMControllerManager的Github主页

对My First Impression of Swift的修改

刚才看了tinyfool的My First Impression of Swift,发现了一些语法错误,所以做一些力所能及的修改。不过因为本人英语水平有限,可能改得不是很好。

Just after Apple announced swift at WWDC 2014, I downloaded Xcode 6 beta 1, but since then I didn’t have time to really learn swift. Today, I watched the WWDC Session videos about swift (Introduction to Swift, Intermediate Swift, and Advanced Swift).

Why Apple makes Swift?

There are so many theories about this. In the WWDC keynote, Craig Federighi said, “Now Objective-C has served us so well for 20 years. We absolutely love it. But we have to ask ourselves the question: what would it be like if we had Objective-C without the baggage of C? “.

Objective-C is a great programming language,  but it is still a very old language with so many baggages.

What is Objective-C without the C?

If we think this question from the angle of programming platform, I think the Objective-C platform includes:

1.Language: Objective-C language.
2.Development tools: Xcode, Interface builder and instrument.
3.Runtime library: Objective-C Runtime.
4.Framework: Cocoa and Cocoa touch.

Apple just changed the language, but still uses the same development tools, runtime library and frameworks. So Swift can change its syntax, but must have so many same  structures with Objective-C underneath. So developers who familiar with Objective-C can learn easily and fast with their old experience.
In the WWDC session I noticed that the speaker said Swift has 3 key design points: safe, modern and power.

So after combing all these things together, I think the purpose of swift is trying to create a safe (strong type), modern (convenient, easy to learn and use) and powerful (doesn’t loss any functionalities from Objective-C) language which doesn’t have any baggages from C, and can be fully supported by Apple’s development infrastructures.

My first impression of the language

1. Swift uses strong type, but the syntax looks just like weak type.

A strong type language is faster and safer than a weak type language, but it also needs more lines of code and looks more complex. On the other hand, a weak type language is convenient, easy to learn and use, but much slower and not very safe. With swift, you can declare the type of variables explicitly like this:

var somestring: String = “this is a string”

You also can declare the type of variables implicitly like this:

var somestring = “this also is a string”

When you don’t explicitly declare the type of variables, the complier will automatically inference it.

2. Swift desn’t use single root class.

Java and Objective-C both use single root class. In Objective-C your classes have to inherit NSObject directly or indirectly. But Swift doesn’t use single root class, your classes can inherit any classes or just never inherit anything.

I think that’s because at the moment when Java and Objective-C are designed, there are so many functionalities that can not be implemented by the complier easily, so just put them to the root class. Now these functionalities can be implemented by the language.

3. You don’t need to use class member variables and properties separately

In Swift, all class member variables are properties, called “stored properties”. And if you want a property that has some setters and getters, it is a computed property.

4. You don’t need to alloc memory for your objects

Yeah!!!!  The complier will do it for you.

5. Structures can have computed properties and methods

But structures still have some differences with classes:

  1. Structures can not inherit other structures.
  2. Structures are passed by value, while Classes are passed by reference.

6. Extensions can extent any classes, named types , or even built-in types

Extensions just like the category of Objective-C, which can be used to extend other classes, but in Swift, extensions can even extend built-in types.

It is convenient but may be dangerous.

7. Optional and Non-Optional type

The optional type gives you the ability to do the safety-checking of variables, making sure they are valid, and it provides a easy and convenient way to do it.

End

Ok, there are more and more syntax and features in the books and the WWDC session videos. But I just want to talk about these, which are easily to learn and may be different to other languages.

NSData转NSString – 如何处理不合法编码问题

在做iOS开发中一个很常见的应用场景就是从服务器接收一段数据然后把它显示出来。但是有时候服务器在数据处理时,比如拼接之类的操作,会出一些问题,造成传过来的数据并不符合指定的编码。(我碰到过的一种情况是,一段宣称用GB18030编码的文字中突然出现了几个用UTF8编码的词语)。浏览器在处理这个问题时,一般就会出现乱码,常用的编程语言在处理这个问题时,也是以乱码显示,而ObjC的NSString则直接返回了一个nil。这一直让我比较头疼,在旧版股吧的开发中曾经碰到因为接口返回的数据里面有一个字节是错的,导致整个接口返回的数据不能用了,当时在国内几个网站上问了别人,得到的都是一些不靠谱的回答。

今天同事又遇到了这个问题,不过他找到了如何让UTF8编码的里面混有错误字节的数据,以容错的方式显示出来而不是nil(见这个Gist),其实原理很简单,一个一个字节读过来,参照UTF8编码的说明,判断是不是合法字节,如果不是,用“?”来代替。根据这段代码,我改了一个用于GB18030编码的混有不合法字节的数据的容错转换。

Gist地址

慎用method swizzling

今天在调试股吧的时候,发现UIImagePickerController右边的BarButtonItem向右边偏移了10个像素。一开始我以为是因为我的UINavigationController的一个category写得有问题导致的,然而我把所有的自定义方法全部弄掉之后,问题还没解决。然后我用了一招更加极端的方法,在applicationDidFinishLoad的地方,把代码全部删掉,把引用的头文件全部删掉,用最原始的UINavigationController和一个空的UIViewController做实验,结果还是偏移了10个像素。这让我百思不得其解,感觉像见鬼了一样。然后我把这个问题交给同事去调,他调了两个小时也崩溃了。

于是我去stackoverflow问了一下。但是大家似乎对这个问题的原因也无法解释,因为我用的是最最基础的代码。一个回答者在下面留言说,最好去查查那个button的大小。虽然UIBarButton是没有frame的,但是我设断点的时候突然发现self.navigationItem.rightButtonItem不是一个Button,而是一个spacing,而且width是-11。但是我奇怪我什么都没做过,为什么会多出来一个spacing,而且宽度是个负数。后来我想起天天基金组前几天在跟财富通组讨论怎么解决iOS7上NavigationBarButtonItem偏移的问题,财富通组的同事说,只要引他们一个头文件就解决了。我想大概是这个头文件引起的问题,它解决了自定义控件向中间偏移的问题,却导致了原生控件向两边偏移的问题。但奇怪的是,我根本没有引用它的头文件,也没调用它的方法,怎么会在我这个地方生效呢?

后来我查了一下那个category,这个是直接引用别人的开源代码。看了代码我就知道是怎么回事了。

这串代码坑爹的地方在于,它把自己的方法和系统的setRightBarButton置换了一下(method swizzling)。于是我明明调用的是系统的方法,而实际上却是调用了它的方法。

更加坑爹的代码在于,method swizzling是在这个类+ load方法里面进行的。只要这个category最后编译进我的代码里,它就会起作用,无论我是不是引入它的头文件。而恰恰财富通组把这个category放在了给我们的framework里面,我们的工程连接了他们的framework,然后这个方法就生效了。

所以说,method swizzling要慎用。虽然objc的动态特性很酷,可以用一些tricks快速解决问题而不需要做大量修改,但是在大型工程里面,特别是涉及到framework的时候,很容易引起side effects。我是iOS组里唯一一个有财富通SVN权限和股吧权限的人,所以我能够发现问题。如果把问题交给别的同事,估计永远就不知道是怎么回事了。

iOS下编译leveldb的总结

之前看到@糖炒小虾_txx 关于如何将leveldb编译到iOS中的文章。自己也试了试,不过还是碰到很多麻烦。

Xcode在build settings里面可以设置使用哪个C++标准库。有两个可以选择,libc++和libstdc++。其中libc++写着是with c++11 support。不过选了libc++之后,deployment target为4.3,就会编译错误,提示libc++只能用于iOS 5.0之后的工程。

所以如果有公司还需要支持iOS4.3的,就必须使用libstdc++。一旦选择了标准库,就需要在任何编译设置里统一它,包括.a文件的编译。

所以我的编译过程如下:

首先,进入leveldb,修改它的makefile,是它支持armv7s和arm64架构:

找到 ifeq ($(PLATFORM), IOS),将下面.cc.o和.c.o中的 -arch armv6去掉,增加 -arch armv7s和 -arch armv64。保留原有的-arch armv7。

然后编译,编译命令改为 CXXFLAGS=-stdlib=libstdc++ make PLATFORM=IOS

编译完成之后,将.a拖到工程里,将leveldb中的include中的头文件拖到工程里。

然后进行Xcode工程设置,在build settings中,将C++ Standard Library改为libstdc++,并且在build phases里面增加连接libstdc++.dylib。这样就行了。

另外,老谭的文章里的demo,里面的AppDelegate不需要把.m改成.mm,只要link了libstdc++.dylib。