Yosemite自定义window的titleView

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

NSView *themeView = [[window contentView] superview];
[themeView addSubview:customView positioned:NSWindowBelow relativeTo:nil];

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

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

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

NSView *themeView = [[window contentView] superview];
NSArray *subViews = [themeView subviews];
NSView *containerView = [subViews objectAtIndex:1];
[containerView addSubview:customView positioned:NSWindowBelow relativeTo:nil];

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

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画边框时,这样的写法可以很容易避开这样问题:

    NSRect rect = xx;
    CGFloat lineWidth = xx;
    NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSInsetRect(rect, lineWidth/2, lineWidth/2)];
    [path setLineWidth:lineWidth];
    [path stroke];

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

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

[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];

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

[NSStatusBar systemStatusBar] _statusItemWithLength:NSSquareStatusItemLength withPriority:USHRT_MAX - 1];

使用上面的方法便可让图标的优先级仅次于系统的图标,放在其它应用程序图标的最右边,但即便如此仍然不能像系统图标一样,按住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这个框架下,通过以下的命令便可将需要的头文件都导出来:

./class-dump -H -o ~/Desktop/NSMenuExtra /System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin 

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

- (void)drawRect:(NSRect)rect
{
    NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:NSInsetRect(self.bounds, 2, 2)];
    if ([_menuExtra isMenuDown])
        [[NSColor blackColor] set];
    else
        [[NSColor redColor] set];
    [path fill];
}

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

- (NSMenu *)menu
{
    return myMenu;
}

- (id)initWithBundle:(NSBundle *)bundle
{
    self = [super initWithBundle:bundle];
    if( self == nil )
        return nil;

    //设置图标的View
    THMenuExtraView *theView = [[THMenuExtraView alloc] initWithFrame:[[self view] frame] menuExtra:self];
    [self setView:theView];

    //创建myMenu和其它一些初使化的工作
    myMenu =[[NSMenu alloc] initWithTitle:@""];
    NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:@"Just Test!" action:nil keyEquivalent:@""];
    [myMenu addItem:quitItem];

    return self;
}

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运行命令提示符(管理员),输入:

mountvol X: /s
bcdboot C:\Windows /s X: /f UEFI

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

打开“终端”,输入:

sudo mkdir /Volumes/EFI
sudo mount -t msdos /dev/disk0s1/Volumes/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中的三个方法新的实现:


//返回是否为替身文件(特意区分了SymbolicLink)
- (BOOL)isAlias:(NSString *)aliasPath
{
    NSURL *aliasURL = [NSURL fileURLWithPath:aliasPath];
    if (!aliasURL)
        return NO;
    
    NSString *fileType = nil;
    BOOL status = [aliasURL getResourceValue:&fileType forKey:NSURLFileResourceTypeKey error:NULL];
    if (!status || !fileType || ![fileType isEqualToString:NSURLFileResourceTypeRegular])
        return NO;
    
    NSNumber *aliasValue = nil;
    status = [aliasURL getResourceValue:&aliasValue forKey:NSURLIsAliasFileKey error:NULL];
    if (!status || !aliasValue)
        return NO;
    
    return [aliasValue boolValue];
}

//返回替身文件的原身
- (NSString *)resolvingAlias:(NSString *)aliasPath
{
    NSURL *aliasURL = [NSURL fileURLWithPath:aliasPath];
    if (!aliasURL)
        return nil;
    
    NSData *aliasData = [NSURL bookmarkDataWithContentsOfURL:aliasURL error:NULL];
    if (!aliasData)
        return nil;
    
    /*
     NSURLBookmarkResolutionWithoutUI 如果原文件是需要挂载的磁盘,挂载的过程中不显示UI
     NSURLBookmarkResolutionWithoutMounting 如果原文件在一个未挂载的磁盘,则不必挂载,但会返回NULL
     NSURLBookmarkResolutionWithSecurityScope 适用于沙盒下使用替身文件(创建时指定了NSURLBookmarkCreationWithSecurityScope)
     */
    BOOL stale = NO;
    NSURL *originalURL = [NSURL URLByResolvingBookmarkData:aliasData
                                                   options:NSURLBookmarkResolutionWithoutUI
                                             relativeToURL:nil
                                       bookmarkDataIsStale:&stale
                                                     error:NULL];
    if (!originalURL)
        return nil;
    
    return [originalURL path];
}

//创建替身文件
- (BOOL)createAlias:(NSString *)aliasPath fromPath:(NSString *)originalPath
{
    NSURL *aliasURL = [NSURL fileURLWithPath:aliasPath];
    if (!aliasURL)
        return NO;
    
    NSURL *originalURL = [NSURL fileURLWithPath:originalPath];
    if (!originalURL)
        return NO;
    
    NSURLBookmarkCreationOptions createOptions = NSURLBookmarkCreationSuitableForBookmarkFile;
    NSData *aliasData = [originalURL bookmarkDataWithOptions:createOptions
                              includingResourceValuesForKeys:nil
                                               relativeToURL:nil
                                                       error:NULL];
    if (!aliasData)
        return NO;
    
    return [NSURL writeBookmarkData:aliasData
                              toURL:aliasURL
                            options:createOptions
                              error:NULL];
}

更多的代码片段可以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:

- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

和NSArray的primitive methods:

- (NSUInteger)count;
- (id)objectAtIndex:(NSUInteger)index;

而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。

<key>SMAuthorizedClients</key>
<array>
    <string>identifier me.tanhao.SMJobDemo</string>
</array>

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

-sectcreate __TEXT __info_plist SMJobHelper/SMJobHelper-Info.plist 
-sectcreate __TEXT __launchd_plist SMJobHelper/SMJobHelper-Launchd.plist

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

<key>SMPrivilegedExecutables</key>
  <dict>
    <key>SMJobHelper</key>
    <string>identifier me.tanhao.SMJobHelper</string>
  </dict>

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

#define kSMJobHelperBunldeID @"SMJobHelper"
- (void)addHelper
{
    NSDictionary *helperInfo = (__bridge NSDictionary*)SMJobCopyDictionary(kSMDomainSystemLaunchd,
                                                                           (__bridge CFStringRef)kSMJobHelperBunldeID);
    if (!helperInfo)
    {
        AuthorizationItem authItem = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 };
        AuthorizationRights authRights = { 1, &authItem };
        AuthorizationFlags flags = kAuthorizationFlagDefaults|
        kAuthorizationFlagInteractionAllowed|
        kAuthorizationFlagPreAuthorize|
        kAuthorizationFlagExtendRights;
        
        AuthorizationRef authRef = NULL;
        OSStatus status = AuthorizationCreate(&authRights, kAuthorizationEmptyEnvironment, flags, &authRef);
        if (status != errAuthorizationSuccess)
        {
            NSLog(@"Failed to create AuthorizationRef, return code %i", status);
        } else
        {
            CFErrorRef error = NULL;
            BOOL result = SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)kSMJobHelperBunldeID, authRef, &error);
            if (!result)
            {
                NSLog(@"SMJobBless Failed, error : %@",error);
            }
        }
    }
}

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

本文Demo下载:SMJobDemo.zip

官方Demo下载:SMJobBless.zip

迟来的2013年终总结

2013年眼看都过去好多天了,看着大家都在写年终总结了,但因为最近工作实在太忙,加上家里有个熊孩子,所以一直抽不开身来总结我的2013。

2013是一个成长年,更是忙碌的一年,不仅对我而言是这样,对我整个家庭来说也是如此,但我为所经历的这一切感到幸运、充实和满足。

首先说说家庭吧,儿子的到来让全家人的重心都发生了很大的变化,回顾这一年来,我能清晰的记得他的每一次成长变化,从学会自己拿东西到爬行,从学会和我们玩耍到叫爸爸妈妈,他成长的每一步也让我看到自己的成长,让我学会如何去照顾别人,让我领悟“责任”二字的份量,让我尝试用更长远的眼光去规划未来。就在前几天迎来了儿子一周岁的生日,因为工作原因都没机会为他庆祝,此处略显无奈。压力更大了,也也算是有了更多的驱动力,未来让我陪伴他一起成长吧!

然后说说工作,其实13年的年初就萌发了改变工作环境的想法了,有这个想法倒不是因为对当时的工作有什么不爽,恰好因为老大对我还不错,才让我迟迟不舍去下定这个决心。在老东家呆了两年,这也是我职业生涯中最重要的两年,这两年让我可以静下心来做一款产品,这个过程对我提升很大。但随着项目逐渐平稳下来,到了一个瓶颈期,我也开始思考自己的未来,我不是一个安于现状的人,同时成为父亲之后我对未来也有了一个新的规划。当然还有一个很重要的外因就是,通过写博客之后认识了更多圈内的新朋友,通过沟通也获得了一些新的机会。 所以13年也为此折腾过好几次,就包括造访了帝都两次,最终获得某公司的offer(此处有内疚,因为各种原因最终放弃了,辜负了为此帮助过我的那些朋友)。最终结果是进了现东家腾讯,也算是一个比较满意的结果吧。

最后说说我的博客,今年博客最大的变化就是用上了自己的主题,虽然比较丑,但好歹是自己的作品,以前用过很多圈内朋友的主题,其中用得最久的就算是小威的主题(与小威交换了友链,也算是博客一大变化吧),全年共写了60篇文章,有50篇是在总结与技术相关的,有几篇在记录我的生活,还有几篇个人写的网评,月平均5篇还是算是比较勤劳了。随着工作和家庭的压力越来越大,以后是否还能这样管理好这个“网络小家”就真难说了。但我很喜欢博客带给我的互联网的归宿,让我不仅仅是这个时代的过客,而是有机会可以真的亲身参与其中。

这13年的总结从13年写到了14年,今天总算是把这事交待清楚了,不过今天也正是假期的第一天,将这个总结在今天完成也算是比较合理。来年注定不轻松,养好精神,为下一个365天而努力奋斗!

JavaScript与Objective-C之间的通信

之前自己闲着无聊写过几个套用网页的“皮包软件”,其实像这样的“皮包软件”也可以做得很高端大气的,通过WebKit实现的OC-JS Bridge,可以非常方便的实现在JS中调用OC的方法/在OC中调用JS的方法。如此一来,便可以通过web实现炫丽的UI,而通过原生API实现与本地相关的操作。

1、JS中变量在OC中的类型
通过OC-JS Bridge,变量的类型会自动进行转换,基本类型都会自动转换,如JS中的number、boolean都会转换成OC中的NSNumber类型,而String类型会自动转换成NSString类型,JS中的对象会转换成WebScriptObject对象,而相关的属性信息可以通过Key-Value的方法读取和写入,本文后面可看到相关的代码。

2、实现在OC中调用JS方法
在OC中调用JS方法是非常方便的,WebView有一个windowScriptObject属性,可以直接获得脚本对象,然后便可以调用callWebScriptMethod:withArguments将消息转发给JS中对象的方法和参数,对于简单的方法调用你也可以直接通过WebView的方法stringByEvaluatingJavaScriptFromString去执行一段JS代码,并返回字符串。示例代码:

//在OC中的调用
- (void)ocAction:(id)sender
{
    NSArray *args = @[@"Hello,JS!"];
    id result = [[webView windowScriptObject] callWebScriptMethod:@"JSFunction" withArguments:args];
    NSLog(@"%@",result);
}
//在JS中对应的方法
function JSFunction(parameter)
{
    //显示OC返回的值
    alert(parameter);
    
    //返回成功的消息
    return 'Web程序已经收到消息!';
}

3、实现JS调用OC的方法
通过设置webView的frameLoadDelegate,在– webView:didClearWindowObject:forFrame:回调方法中,指定一个本地对象(该对象实现WebScripting协议),然后JS中就可以直接调用该对象的相关方法。
OC中的代码如下:


//该方法用于JS中调用
- (WebScriptObject *)status:(WebScriptObject *)jsObject
{
    //将JS发过来的信息显示出来
    NSString *message = [jsObject valueForKey:@"message"];
    NSLog(@"%@",message);
    
    //返回成功的信息(WebScriptObject对象不能自己创建,所以此处复用了传入的参数)
    [jsObject setValue:@"本地端已经收到消息啦!" forKey:@"message"];
    return jsObject;
}

#pragma mark -
#pragma mark WebFrameLoadDelegate

//通过此回调,将self传递给JS环境
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
{
    [windowObject setValue:self forKey:@"native"];
}

#pragma mark -
#pragma mark WebScriptingProtocol

/*
 返回是否阻止响应该方法,
 返回NO即能响应该方法
 */
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector
{
    if (selector == @selector(status:))
    {
        return NO;
    }
    return YES;
}

/*
 返回本地方法在JS中的名称(不实现此方法,则JS中方法名与OC中相同)
 */
+ (NSString *)webScriptNameForSelector:(SEL)sel
{
    if (sel == @selector(status:))
    {
        return @"ocMethod";
    }
    return nil;
}

JS中代码如下:

function CallNative()
{
    if (native)
    {
        //将消息组装成对象发给OC
        var parameter = {'message':'Hello,Objective-C!'};
        var result = native.ocMethod(parameter);
        
        //显示OC返回的结果
        alert(result['message']);
    }
}

相关的示例代码可以参见Apple的CallJS这个Demo,也可以直接下载我写的Demo:OCJS_Bridge.zip

修改其它程序的defaults文件

在iOS和Mac开发的程序中,我们常用NSUserDefaults来保存一些配置信息,我们都知道这是一种基于Plist文件的存储,并且最终保存的信息存放在/Library/Preferences/或~/Library/Preferences/目录之下,以BundleID命名的plist之中。

在Mac下我们通过修改系统程序配置来开启一些隐藏设置,如开启Finder显示隐藏文件功能,就需要去修改Finder程序的配置文件的AppleShowAllFiles这个键值,你可能想到直接修改~/Library/Preferences/com.apple.finder.plist文件来达到目的,但是由于NSUserDefaults会有一套缓存机制,所以对于正在运行的程序你直接修改这个文件极有可能是不会成功的,当然如果Finder未启动时,你这样做就是OK的,但像Finder和Dock这样的程序是通过launchd启动的,所以它是一直处于运行状态,你就只能另辟蹊径了。

在OSX系统下,有一个defaults的工具,使用它就可以很方便的去修改这个值并且保证有效,比如上面提到的开启Finder显示隐藏文件功能,我们可以这样:

//设置Finder的配置项值
system("defaults write com.apple.finder AppleShowAllFiles -bool true");
//重启Finder
system("killall Finder");

但使用defaults有很大的局限性,比如想读取或修改结构复杂的键值(NSArray和NSDictionary),恰好我在一个项目中就遇上这样的需求,通过查找资料我发现有更加方便的的API,那就是Carbon接口中的CFPreferences相关方法,由于它是原生的方法,不直接依赖于其它程序,所以是一个非常好的选择,上面的代码修改为:

//读取当前的值,读取前先同步
CFPreferencesAppSynchronize(dockID);
CFBooleanRef valueRef = CFPreferencesCopyAppValue(key, dockID);
CFBooleanGetValue(valueRef);

//设置新的值,修改后同步
CFPreferencesSetAppValue(CFSTR("AppleShowAllFiles"),
                         CFBooleanGetValue(valueRef)?kCFBooleanFalse:kCFBooleanTrue,
                         dockID);
CFPreferencesAppSynchronize(dockID);

//重启Finder
system("killall Finder");

以上测试代码Demo下载:PreferencesDemo.zip