Objective-C实现的简单逻辑表达式解析

因为项目中会用到远程配置,配置内容会进行很多条件判定,为了让配置更灵活,所以希望最终能将条件满足情况直接进行逻辑表达式运算.
所以就涉及到表达式的解析,开源的表达式解析的库倒不是少,但考虑仅是逻辑表达式的处理,便不想引入庞大的外部库,于是就决定自己来造这个轮子.
简单分析,其实需要处理的表达式一共就包含这7种符号:&&,||,!,(,),YES,NO,并且相互之间的优先级也非常简单.
将解析的过程分解为如下几个步骤:
1.从表达式中按顺序读取并匹配符号;
2.当匹配到的是(,&&,||,!都直接放于栈中,继续步骤1;
3.当匹配到的是YES或NO,就依次从栈中取出前面的符号,如果是!取反后放入栈继续此步骤,如果是&&或||就再取前一个操作数作逻辑运算,并继续重复此步骤,直到栈内没有内容或遇上(便将当前操作结果的数放入栈继续步骤1;
4.当遇上)则取出栈顶的YES或NO,并将前一个(取出扔掉,然后继续执行步骤3.
5.当表达式解析到末尾,栈内便只有栈顶的这个结果.

最终代码实现,这可能不是最优的方案,代码也不是很简洁,但问题算是解决了:

解决NSDistributedLock进程互斥锁的死锁问题(二)

上一篇文章中介绍了采用了文件记录锁来实现更加安全的多进程互斥,它的平台兼容性也非常好,并且我们也采用它实现了NSDistributedLock的所有的方法.
其实在OSX还可以采用文件读写锁来实现更加方便的进程互斥,在fcntl.h中我们可以看到这样的宏定义:

这些宏是同O_RDONLY,O_WRONLY等一样,都是用于打开文件时使用的掩码,也就是说我们可以在open文件的时候加上这些掩码来实现读写互斥,具体的规则就是(适用于多进程或多线程):
只要没有写模式下的加锁,就可以进行读模式下的加锁;
只有读写锁处于不加锁状态时,才能进行写模式下的加锁;

当然我们大多数情况下都只是简单的资源独占的互斥,所以直接采用写模式下的互斥便可,例如当进程A有如下执行了操作:

其它进程再要做同样操作时就会进入阻塞状态,直到进程A中close了文件描述符或进程退出.

因为同样适用于多线程,所以我们可以实现以下阻塞版本的NSDistributedLock:

解决NSDistributedLock进程互斥锁的死锁问题(一)

在MAC下的多进程开发中,NSDistributedLock是一个非常方便的互斥锁解决方案,一般的使用方法:

但在实际使用过程中,当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.这是使用NSDistributedLock时非常隐蔽的风险.其实要解决的问题就是如何在进程退出时会自动释放锁.
在《Unix环境高级编程》中有对record locking(记录锁)的介绍,而这种锁的机制也是基于文件,但它的解锁条件刚好可以解决我们的问题,因为当进程退出时记录锁会自动释放所持有的锁.

fcntl是Unix兼容最好的一种记录锁实现方案,关于它的接口描述在sys/fcntl.h内,函数原型是

该函数用于记录锁时,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。

关于fcntl函数的三种命令描述:
F_GETLK 取得文件锁定的状态。
F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。

用flock实现NSDistributedLock中tryLock:

用flock实现NSDistributedLock中unlock:

但需要注意的是fcntl实现的记录锁是不支持进程内的多线程间的互斥,为了解决这些问题并完整实现NSDistributedLock中的所有方法和实际表现,下面是我封装的QMDistributedLock:
站内下载:QMDistributedLock.zip

Yosemite自定义window的titleView

在OSX系统中对Windows的自定义向来都是一个很麻烦的事情,从事MAC开发的同学应该都深有体会.
在过去,如果我们需要在标题栏中添加自定义的控件,可能通过下面的方式:

不过这样的做法在使用Yosemite的SDK编译时会产生这样的警告:
titleView_error

虽然这个警告并不会造成显示的异常.
我特意测试了一下Apple的兼容性设计,我发现使用10.9的SDK编译就不会产生这样的警告,通过打印window的tree结构发现一个奇怪的现象:不同的SDK编译的程序中的window的组成结构是一样的?后来发现在Info.plist中的DTSDKBuild项与编译的SDK相对应,在运行时会作为环境变量决定系统采取什么样的兼容策略.

回到正题,那如何在Yosemite中添加自定义的元素titleView呢,根据警告内容,NSThemeFrame不能添加未知的View,通过打印NSThemeFrame的子视图可以发现它包含两个视图,一个就是window的contentView,另一个是NSTitleBarContainerView,这个视图是关闭,最小化,缩放等按钮的容量,经试验放在它上面是可行的.

代码跑起来发现添加上去的控件处于半透明的状态,通过查询window的文档,发现有一个titlebarAppearsTransparent的属性,将它置为YES,问题解决.

另外附上一个可以统一设置标题和窗口背影色的window,能兼容不同的系统版本:
CustomWindow.zip

由一个像素引发的”血案”

由于项目中经常需要程序绘制各种奇形怪状的自定义控件,所以NSBezierPath是一个非常不错的选择,偶然的机会发现当设置NSBezierPath的lineWidth为1.0时,绘制出来的效果很不理想,如下图所示:
NSBezierPath1

这当然不是我所想要的效果,线条模糊,宽度似乎也不是我想要的1.0个像素,经过多次尝试,发现当NSBezierPath的x坐标和y坐标均处在0.5的位置时效果就令人满意了:
NSBezierPath2

最初我也不求甚解,知道如何应对便可,但每次代码写到此处时,好奇心便又多了一点儿,虽然根据自己经验已经能猜到大概,但仍然去拜读了一下Apple的文档,结果正如心中所料:当我们用整数定义某路径时,路径的坐标总是在两个像素之间的,而NSBezierPath在绘制线条时,线条宽度刚好平均分配在路径两侧,而当宽度为1或其它奇数时,两侧就会出现半像素的情况,并且在非Retina显示下,每一个需要绘制的像素是与屏幕最终显示的单元是一一对应的,如图:
NSBezierPath3

所以结果就导致原本一个像素的内容最终呈现在了两个像素的显示单元上了,最终造成了显示效果并非我们所想要的.而当宽度为偶数时,就恰好可以避免这样的情况:
NSBezierPath4

而当我们将位置设定在0.5个像素的时候,正好可以在路径两边都占用整数个显示单位,如下图:
NSBezierPath5
问题也就迎刃而解了,所以在平时使用NSBezierPath画边框时,这样的写法可以很容易避开这样问题:

MAC系统状态栏通过插件添加图标MenuExtra Plugin

在开发MAC软件时,为是让用户更方便的使用功能并且不占用过多桌面空间,我们一般都会选择在状态栏添加图标,就如Windows上的任务栏一样.我们通常使用如下的方式的生成状态栏图标:

当然这也是推荐的方式,因为采用的是标准的API,对于需要上架MAS(Mac App Store)的软件来说,这也是唯一的方法.但这样生成的图标优先级低,当左边应用程序的菜单项过多时很容易就被遮挡而不显示,对于不上架MAS的软件来说,我们可以使用以下的Private API:

使用上面的方法便可让图标的优先级仅次于系统的图标,放在其它应用程序图标的最右边,但即便如此仍然不能像系统图标一样,按住command就能随便拖动位置,并且在点击之后也不能像系统图标一样左右自由切换,所以还有一种方式就是采用插件的方式.

系统的状态栏图标由SystemUIServer这个系统进程负责管理,它是通过插件的方式展示每一个系统图标,我们可以在/System/Library/CoreServices/Menu Extras这个目录下面找到所有的系统状态栏图标.而这里面每一个menu扩展名的bundle就是状态栏图标插件.称之为MenuExtra plugin.但是在10.2以后的,MAX OSX就不再允许第三方的MenuExtra plugin,而此时我们就需要一个开源的项目,叫做MenuCracker,它本身也是一个MenuExtra plugin,但是当加载了这个插件之后,便开启了第三方MenuExtra plugin的支持,它的项目托管在sourceforge,地址是:http://sourceforge.net/projects/menucracker/,至于这个项目的稳定性,可以在sourceforge看到它已经有十年的历史了,所以…

当然MAC OSX系统并未开放MenuExtra plugin的开发,Xcode也没有相应的模板,标准的API也没有任何支持.所以如果我们要开发MenuExtra plugin就只能采用Private API,所以需要上架MAS的软件或拒绝使用Private API就可以打住了,下面就是实际动手来开发一个简单的MenuExtra plugin.

1.首先通过class-dump导出Private API的头文件
class-dump对于做MAC OSX开发的童鞋来说应该都不陌生了,这是下载地址:http://stevenygard.com/projects/class-dump/
而我们所需要的头文件都在SystemUIPlugin.framework这个框架下,通过以下的命令便可将需要的头文件都导出来:

2.在Xcode中创建插件的工程
在Xcode中选择Bundle这个模板,然后在Build Settind中将Wrapper Extension项修改为”menu”.
将步骤1中导出的头文件添加到工程中(删除类似这样的#import “NSObject.h”无用的头文件引用,因为可能编译会出错).
在工程的Link Binary With Libraries中引用/System/Library/PrivateFrameworks/SystemUIPlugin.framework这个框架(直接拖进来即可).
创建用于显示图标的View的类,让它继承于NSMenuExtraView,然后让它画上你想要的图形,比如:

创建图标这个对象,也是这个插件的Principal class,让它继承于NSMenuExtra(NSStatusItem的子类),比如我们类名叫THMenuExtra,那么也将工程的info.plist中将Principal class的值设置为THMenuExtra,这是插件创建的入口.
THMenuExtra类需要去重写父类的以下两个方法:

3.插件安装及卸载
如上文所描述的,如果需要让我们的插件跑起来,肯定需要先行安装MenuCracker.menu(双击即可),如果我们有其它外部程序,可以将MenuCracker打包我们的程序之内,并拷贝到用户合适的目标再open它.
完成了上一步骤,我们插件的安装就非常简单,与MenuCracker的安装一样,直接Open即可.
但卸载略麻烦,你可以在插件中将自己unload完成卸载,或者将插件删除,然后结束SystemUIServer进程即可(SystemUIServer会自动重新启动).
插件的更新,如果你修改了插件的内容,即便插件自身已经unload,你双击新的插件,你会发现仍然是旧的版本,那是因为SystemUIServer会自动缓存你的Bundle,所以如果需要更新的话,你依然需要结束SystemUIServer进程才可完成.

以上插件Demo源码请访问:https://github.com/tanhaogg/THExtraMenu

解决MacBookPro在EFI模式下使用Windows8.1声卡驱动问题

最近因为要使用Windows系统,所以索性就在我的MacBookPro上安装了Windows8.1,因为Windows8.1可以支持EFI模式安装,所以各个过程非常方便,但安装之后唯独声卡驱动不了,安装最新BootCamp的驱动也不管用,最后终于找到别人给出的解决方法,转载记录一下,原帖地址:http://www.pcbeta.com/forum.php?mod=viewthread&tid=1501058

MacBookPro支持EFI安装和启动Windows8的消息出一经传出,其启动快速、原生AHCI等诱惑让发烧友们纠结于Bootcamp和EFI之间。相较于苹果推荐的Bootcamp模式,EFI的优势是显而易见的:
Bootcamp方式:

优点:系统装完后安装一下Bootcamp驱动就OK了,什么都不用管。

缺点:对于换了SSD硬盘的用户,没有AHCI就没有Trim,没有快速启动,或有了AHCI(更改MBR程序)有了Trim,但不能睡眠(唤醒死机),还是没有快速启动。
EFI方式:

优点:原生AHCI、快速启动(换了SSD的5秒开机)

缺点:声卡驱动失败。

对于纠结于Bootcamp和EFI的朋友来说肯定是已经搞清楚安装和设置等问题的了。声卡驱动问题,本人曾发文介绍过解决办法,但有些朋友反映遇到了麻烦,经过再次验证,并对步骤简化。下面先说说解决的原理:即在Windows下创建启动文件并(在Mac OS下)将引导文件bootmgfw.ef放入 EFI 分区中的EFI\Microsoft\Boot 目录下,替换微软的引导工具即可,方法如下。请先将你的MBP设置为win8默认启动。
1、Windows8下创建启动文件

Ctrl+X运行命令提示符(管理员),输入:

显示“已成功创建启动文件”,重启电脑进入Mac OS,然后:
2、Mac OS下替换bootmgfw.efi文件:

打开“终端”,输入:

桌面上(或Finder里)出现EFI分区图标,复制附件bootmgfw.efi粘贴至\EFI\Microsoft\Boot替换,重启,OK。
注意:

1 必须默认为windows启动才有效,即使按上述方法解决了声卡驱动问题,如果你修改默认启动为Mac OS或option启动Windows的话,声卡驱动将再次失效。

2 在EFI模式下完成Win8系统安装后,切记要手动安装Bootcamp5,该Bootcamp对MBP的硬件有着极好的支持,无需其他任何驱动程序。

文中的附件下载:bootmgfw.zip

对替身(Alias)文件的操作

对于替身文件的操作,以前我们可以直接使用CarbonCore/Aliases.h这个头里面的方法,比如FSIsAliasFile,FSResolveAlias,FSNewAliasFromPath等方法来判定是否是替身文件,找到替身的原文件和创建替身,但这些方法在10.8系统上都已经DEPRECATED,所以目前最好都使用更高级的API来对替身操作.在更高级的API中我们就会发现,原来Alias会被用在Bookmark之中,所以通过对NSURL中Bookmark相关的方法就可以方便的处理替身文件了,以下就是对应上文中的FS中的三个方法新的实现:

更多的代码片段可以watch我GitHub上的这个Repository:https://github.com/tanhaogg/THCategory

线程安全的可变容器类

稍稍有点儿Objective-C编程经验的人都明白NSMutableArray和NSMutableDictionary此类可变的容器都是线程不安全的。但即便大家都知晓这个准则,但在使用多线程编程时,我们仍然会通过可变容器来达到线程间的数据共享,当然作为富有经验的程序员,你肯定会想法设法让这种不安全变得安全,比如用锁、或者将容器的操作切换到同一个线程或串行的dispatch_queue中。但有时原本很简单的逻辑却因为这严谨的安全让这一切变得复杂起来,这时我便希望有一个安全的容器类让代码显得更加优雅。

好吧,我的想法就是继承原可变容器构建一个安全的子类,虽然前辈们都不推荐这样做。Cocoa的可变容器有一个高端特性被称之为Class Clusters,关于这个特性的具体解释我摘抄了lianxu博客中的文字:

在 Cocoa 中有一种奇葩的类存在,有程序员抨击它是 OOP 模式的破坏者,这就是 Class Clusters。面向对象的编程教育我们:“类可以继承,子类具有父类的方法”。而 Cocoa 中的 Class Clusters 虽然平时表现的像普通类一样,但子类却没法继承父类的方法。而 NSMutableArray, NSMutableDictionary 就是这样一个玩意。为何如此?因为 Class Clusters 内部其实是由多个私有的类和方法组成。虽然它有这样的弊端,但是好处还是不言而喻的。例如,NSNumber 其实也是这种类,这样一个类可以把各种不同的原始类型封装到一个类下面,提供统一的接口。这正设计模式中的抽象工厂模式。

查看Apple的文档,要继承这样的类需要必须实现其primitive methods方法,实现了这些方法,其它方法便都能通过这些方法组合而成。比如需要继承NSMutableArray就需要实现它的以下primitive methods:

和NSArray的primitive methods:

而NSMutableDictionary也类似,在Apple文档中都有详细的描述。当然除此之外你都可以选择性的实现其它方法,以达到更高效率,比如NSMutableArray的removeAllObjects方法,因为默认他将循环调用removeLastObject方法达到目的,而你则可以选择更高效的实现方式。而要达到线程安全,不外乎就是在这些方法内部都加上锁,简化多线程情景下的容器使用,不必手动逐一添加锁。

上面已经详细描述了派生线程安全的可变容器,实现其实已经很简单了。但既然要实现线程安全的容器类,并且是Apple和前辈都不推荐的做法,这必须有一定的原因,而这原因就是程序的公敌————性能问题。经过简单的测试,性能会发生倍数的影响,不过因为容器性能好,基数小,所以性能尚且还能接收。但当你使用容器来频繁的处理大量数据则不推荐这样选择,仅当线程间的同步成了数据共享的瓶颈时,一个安全的容器类才有存在的价值。

相关的代码及测试Demo:https://github.com/tanhaogg/SafeContainer

通过ServiceManagement注册LaunchdDaemon

在OSX开发与系统密切相关的软件,经常会使用到高权限的操作,让整个程序都以高权限执行显然是不合适的,通过将高权限的任务封装到独立的子程序,按需要进行调用是比较合理的做法。而常见的做法是通过注册LaunchdDaemon,便可轻松实现开机启动、长驻后台、高权限的需求。

注册LaunchdDaemon的常用方法是通过launchd工具去load一个与Daemon程序相关的标准的plist文件,这个过程一般放在PKG的安装脚本中完成,但当前越来越多的软件摒弃了PKG的打包方式,而是直接选择了打包成app,这样确实提升用户体验,但安装辅助工具的工作都放到了app运行过程中了。由于launchd是需要高权限运行的,而app中提权运行工具并不那么方便。所以便可以考虑通过ServiceManagement的API来完成这样的操作。

ServiceManagement是在10.6就出现的FrameWork,下面就具体介绍一下如何使用相关的API来注册高权限的LaunchdDaemon。

首先你需要在你的工程中创建一个用于LaunchdDaemon的命令行程序的Target,然后为主程序添加工程依赖,并Copy Files中将此子程序拷贝到Contents/Library/LaunchServices目录下。

然后为该子程序添加两个配置文件,一个是程序环境相关的Info.plist,它与普通app中的info.plist一样,添加上一些必须的字段如BundleID,额外需要添加一项SMAuthorizedClients,用于标识可加载或卸载它的主程序,形如下面的代码,指定了它的主程序BundleID为me.tanhao.SMJobDemo。

而另一个是用于Launchd加载时的配置文件,但与以前通过launchd工具去加载的那个plist不同的是不需要添加ProgramArguments项,这些字段都会在注册成功后自动填充。
由于这是一个命令行工具,所以要将两个plist文件编译进Mach-O文件之中,就需要在工程配置中的“Other Linker Flags”中添加额外的参数,形式如下:

主程序要能匹配到合适的子程序,也必须在Info.plist再添加SMPrivilegedExecutables项,用于指定它可以安装的LaunchdDaemon子程序,形如下面的代码,指定

然后我们就来完成主程序中注册LaunchdDaemon的代码:

编译并运行以上代码(必须签名),首先会弹出一个系统的权限框,然后你输入管理员密码,你的LaunchdDaemon就注册成功了。你会发现子程序被拷贝到了/Library/PrivilegedHelperTools目录,而在/Library/LaunchDaemons也生成了一份与之对应的配置文件,用于launchd进程在合适的时机调用。

本文Demo下载:SMJobDemo.zip

官方Demo下载:SMJobBless.zip