Sucha's Blog ~ Archive for December, 2014

14年12月29日 周一 10:22

My First Mac App - TcBrowserMac

开始练习 Mac App 的编写,与 iOS App 有许多不同,昨天花了一天时间,山寨了 一个 TcBrowser,一个筛选算法题目的小软件,作者博客页面 Top Coder算法题目浏览器

也许是我没有深入开发的原因,感觉不再像 iOS 一样,需要多个 View Controller 了,使得我一开始就考虑模块划分的问题,虽然这个软件是个小工具 而已。

然后考虑像 win 一样,可以在文件夹下面打开访问当前文件夹的目录,由于 Mac 是打包成 bundle 来跑的,界面上的 app 其实是个文件夹,所以我之前整的方法 都不对,需要通过 [[NSBundle mainBundle] bundlePath] 来获取。

然后 StoryBoard 方面,由于软件是个窗口,所以大小是可变的,一开始就得考虑 自动布局的问题,Apple 文档那边对于自动布局长篇大论不说,还有一个用于自动 布局的 DSL,足够复杂了,昨天时间有限,我是没有看透。

实际是用 StoryBoard 点击 nib 后 Editor -> Resolve Auto Layout Issues 上 面的 Add Missing Constraints 来做的,每添加一个控件,就要 Add 一次。

然后感觉还跟控件添加的顺序有关系,Xcode 会猜测这个控件到底是对齐那一边, 依据的是 superview 还是哪个 view,这些 constraints 在 inspector 里面几乎 不能配置,我本来一个 webview 靠右是要左对齐的,然后右边让它跟着 superview 无限扩展,可是如果给弄成右对齐就改不了了,反正我是没弄懂怎么改, 所以我只好把 constraints 删除又添加一遍。

然后是 tableview,与 iOS 也有些不同,没有了 cell 的概念,取而代之的是 NSView,没有了 index,而是一个 NSTableColumn 的结构,table column 的 identifier 可以设置,重用时也是依据这些 identifier。不过 column 的 header title 我折腾了不少时间,然后是宽度也是通过 column 设置的。

跟 iOS 一样,只需要设置两个函数就可以让数据上来了,一个是 numberOfRowsInTableView,一个是 viewForTableColumn。

最后是软件图标的问题,感觉与 iOS 也不一样,也许是我之前没有那么正规军的 原因。我是想要 Dock 上面自定义图标,谷歌到的方法也只是动态设置的而已,关 闭的时候很明显会显示回默认的那个。

实际上呢,需要设置的方法在 Apple Doc - Optimizing for High Resolution, 具体是需要下面几个大小的图标,然后放到 icon_name.iconset 这个文件夹下面

icon_16x16.png
icon_16x16@2x.png
icon_32x32.png
icon_32x32@2x.png
icon_128x128.png
icon_128x128@2x.png
icon_256x256.png
icon_256x256@2x.png
icon_512x512.png
icon_512x512@2x.png

终端下使用命令

$ iconutil -c icns icon_name.iconset

来生成,然后拖到工程文件里面就好了,默认就是那个图标了,这个也折腾了不 少时间 ,第一个 Mac App 终于完成了,地址在这里 TcBrowserMac,密码是宇宙终极答案。

14年12月27日 周六 17:58

ObjC Animation

其实去年在做那个 App 的时候就有使用过简单的动画,接口是 UIView 的 animateWithDuration,只是平移了一下 view。

今天学到的 iOS 的动画,分成几个部分,分别是针对 view 的,针对 controller 的过渡动画,还有就是自由度很高的交互动画,交互动画其实可以看成是最简单的 游戏了。

针对 view 的动画除了我之前用的 animateWithDuration 方法,其实 view 的很 多属性都是可以更改数值然后交给系统去实现动作的,只需要创建 animation 设 置动画属性,然后使用 view 的 layer 实体 addAnimation 就好。

view 中的 layer 需要说一下,为了实现动画,有多个副本,一个是表示最终状态 的(model layer),一个是表示的是当前的(presentation layer), presentation 是专门用来显示动画的,结束后就被设成了 model 的状态。

后面例子的还用画图实现了一个动画,画图部分就纯是 CPU 来计算了。另外一个 部分是 controller 的转场动画,例子比我之前的平移复杂了一些,用了系统给出 的 delegate 接口来设置,总之这也是可以自定义的。

还有最后的可交互动画,实际上就跟游戏没啥区别了,动画部分的数值需要不断根 据用户的交互来获取、计算,然后设置具体的显示,这些计算实际上是在显示驱动 的每一帧间隔里面做的。

cocos2dx 的底层部分也是通过 display link 来驱动实现每一帧的画图与计算, 然后用户的输入是通过 UIView 的 UIKeyInput delegate 来做的,而不像这个例 子通过手势控件 recognizer 来做的, cocos2dx 的方式更灵活一些,不过两者对 于动画显示、操作最底层的部分是相似的。

没想到两者后底层部分的驱动结构其实是一样的,display link 与 run loop。

14年12月25日 周四 22:16

ObjC Core Data

Core Data 市面上的文章太多,不管是中文还是英文,所以打算以自己的理解从简 描述。

先从上到下、从大到小理解概念。关键的概念其实只有 4 个:

如果还要简约,那概念上只需要关注 model 跟 store 就可以了,一个是 schema, 模型结构,比如一张表,另一个是数据库,具体到文件。

描述概念间相互关系的图表网上太多,我简略看了一下接口,了解到的是目前 8.1 的 SDK下,一个 context 对应一个 model 和一个 coordinator,一个 coordinator 可以连接多个 store,相互的关系可以脑补了吧。

下面的代码只要建立一个 TestCoreData 的头文件调用一下最下面的 test 就可以 编译跑了,粗看有点多,其实分开函数看就好了,函数名很直白了。

@interface UserInfo : NSManagedObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSNumber *uid;
@end

@implementation UserInfo
@dynamic name;
@dynamic uid;
@end

@implementation TestCoreData
- (NSURL*)createStoreDir
{
    NSFileManager *fm = [[NSFileManager alloc] init];
    NSError *error = nil;
    NSURL *durl = [fm URLForDirectory:NSDocumentDirectory
                             inDomain:NSUserDomainMask
                    appropriateForURL:nil
                               create:YES
                                error:&error];
    if (durl == nil) {
        NSLog(@"Fail to create document dir:%@", [error localizedDescription]);
        return nil;
    }
    return durl;
}

- (NSManagedObjectModel*)createModel
{
    NSManagedObjectModel *moModel = [[NSManagedObjectModel alloc] init];
    
    // create the entity
    NSEntityDescription *infoEntity = [[NSEntityDescription alloc] init];
    [infoEntity setName:@"UserInfo"];
    [infoEntity setManagedObjectClassName:@"UserInfo"];
    
    [moModel setEntities:[NSArray arrayWithObject:infoEntity]];
    
    // add the attribute
    NSAttributeDescription *nameAttr = [[NSAttributeDescription alloc] init];
    [nameAttr setName:@"name"];
    [nameAttr setAttributeType:NSStringAttributeType];
    [nameAttr setOptional:NO];
    
    NSAttributeDescription *uidAttr = [[NSAttributeDescription alloc] init];
    [uidAttr setName:@"uid"];
    [uidAttr setAttributeType:NSInteger32AttributeType];
    [uidAttr setOptional:NO];
    
    // set the properties for the entity
    NSArray *ary = [NSArray arrayWithObjects:nameAttr, uidAttr, nil];
    [infoEntity setProperties:ary];
    
    // add localization dictionary
    NSMutableDictionary *localDict = [NSMutableDictionary dictionary];
    [localDict setObject:@"name" forKey:@"Property/name/Entity/UserInfo"];
    [localDict setObject:@"uid" forKey:@"Property/uid/Entity/UserInfo"];
    
    [moModel setLocalizationDictionary:localDict];
    
    return moModel;
}

- (NSManagedObjectContext*)createModelObjectContext:(NSManagedObjectModel*) moModel
{
    NSManagedObjectContext *moMOC = [[NSManagedObjectContext alloc] init];
    NSPersistentStoreCoordinator *co =
           [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:moModel];
    
    [moMOC setPersistentStoreCoordinator:co];
    
    NSString *store_type = NSBinaryStoreType; // NSSQLiteStoreType
    NSString *store_filename = [NSString stringWithFormat:@"storeExample.bin"];
    
    NSError *error = nil;
    NSURL *store_url = [[self createStoreDir]
           URLByAppendingPathComponent:store_filename];
    
    NSLog(@"store url: %@", [store_url absoluteString]);

    NSPersistentStore *pstore = [co addPersistentStoreWithType:store_type
                                                 configuration:nil
                                                           URL:store_url
                                                       options:nil
                                                         error:&error];
    if (pstore == nil) {
        NSLog(@"Fail to create persistence store %@", [error localizedDescription]);
        return nil;
    }
    return moMOC;
}

- (BOOL)test
{
    NSManagedObjectModel *moModel = [self createModel];
    NSManagedObjectContext *moMOC = [self createModelObjectContext:moModel];
    
    NSEntityDescription *infoEntity =
          [[moModel entitiesByName] objectForKey:@"UserInfo"];
    
    // insert
    int i;
    for (i=0; i<10; i++) {
        UserInfo *info = [[UserInfo alloc] initWithEntity:infoEntity
                           insertIntoManagedObjectContext:moMOC];
        info.name = [NSString stringWithFormat:@"n%d", i];
        info.uid = [NSNumber numberWithInt:i];
    }
    
    // save
    NSError *error = nil;
    [moMOC save:&error];
    if (error) {
        NSLog(@"Fail to save: %@", [error localizedDescription]);
        return false;
    }
    
#if 1
    // query & delete
    NSFetchRequest *req = [[NSFetchRequest alloc] init];
    [req setEntity:infoEntity];
    
    NSSortDescriptor *sdesc = 
          [[NSSortDescriptor alloc] initWithKey:@"uid" ascending:YES];
    [req setSortDescriptors:[NSArray arrayWithObject:sdesc]];
    
    NSArray *ary = [moMOC executeFetchRequest:req error:&error];
    if (error && (ary==nil)) {
        NSLog(@"Fail to fetch object: %@", [error localizedDescription]);
        return false;
    }
    for (UserInfo *info in ary) {
        NSLog(@"user info name:%@, uid:%d", info.name, [info.uid intValue]);
        [moMOC deleteObject:info];
    }

    // save again
    [moMOC save:&error];
    if (error) {
        NSLog(@"Fail to save: %@", [error localizedDescription]);
        return false;
    }
#endif
    return true;
}
@end

更具体细节的东西,比如 Predicates、搜索结果的缓存,可以需要的时候再细查 了,有了重要概念、框架的支撑,往下往细里面走就好办了吧。

明天玩玩新的东西。

14年12月24日 周三 15:29

NSThread、NSRunLoop、GCD、__block

因为想求职 Objective-C 开发的原因,被问到了一些很基础的问题,而我做 iOS App 已经是一年以前的事情,且也没有那么系统地学习过,所以就慢慢把这些补齐。

我是从 POSIX 的 pthread 入门了解多线程的,用过 mutex 这些临界区保护的方 法,对于 Mac Objective-C 这边的多线程,对应的是 NSThread。

建立多线程是为了同步做一些事情,不影响界面的响应等等,这些事情要么是纯计 算的,要么是各种事件的处理,对于这些事件的检测以及处理,Objective-C 是放 到 NSRunLoop 里面去的,感觉跟满足了 select 以及 epoll 条件下的大括号块差 不多吧。

NSRunLoop 就是一个内部循环的结构,一定是从属于某个线程的,要么是属于主线 程,要么是属于自己创建的线程。

下面的例子是建立了一个 NSRunLoop 响应一秒一次的 NSTimer,这个 NSRunLoop 是跑在一个单独的线程(非主线程)里的。

- (void)timer_entry:(NSTimer*)t
{
    NSLog(@"timer is main thread %d", [NSThread isMainThread]);
}

static int _newThreadAborted;

- (void)thread_entry:(id)param
{
    NSLog(@"enter thread");
    @autoreleasepool {
        NSTimer *t = [NSTimer timerWithTimeInterval:1
                                              target:self
                                            selector:@selector(timer_entry:)
                                            userInfo:nil
                                             repeats:YES];
        NSRunLoop *r = [NSRunLoop currentRunLoop];
        [r addTimer:t forMode:NSDefaultRunLoopMode];
        while (!_newThreadAborted) {
            [r runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            NSLog(@"should not reach here until loop ending");
        }
        NSLog(@"thread aborted");
    }
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    
    NSLog(@"is main thread %d", [NSThread isMainThread]);
    
    NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(thread_entry:) object:nil];
    [t start];
}

上面的例子 NSRunLoop 添加了一个 NSTimer 事件源,同样的可以添加其他的事件 源,比如鼠标、键盘的,这样就可以在某个线程的 NSRunLoop 里面处理具体的事 情了,且不影响主线程。

如果 NSRunLoop 里面 while 了一个死循环,这个线程就做不了其他事情了。如果 这个 NSRunLoop 是在主线程里面,界面就会卡住,一个 NSThread 里面只能有一 个 NSRunLoop,新建立的线程会在调用获取 NSRunLoop 的时候创建。

而 GCD (Grand Central Dispatch) 我觉得中文网络上面的信息挺不错的,也许 也是因为自己之前已经使用了不少了吧,也因为使用的简单,所以当时的项目里面 也让我不需要去关注 NSThread 跟 NSRunLoop 了。

具体的使用其实就是构建一个闭包函数,跟 Lua 里面的闭包效果一样,所以这样 的函数也可以作为值传递了。

比较需要注意的是栈上的闭包函数与堆上的闭包函数的区别,其生命周期是不一样 的。

另外就是闭包内部访问外部的变量值,若是栈上的变量,则是取建立闭包时的值, 若无 __block 修饰,则是建立闭包函数时拷贝到闭包结构里面的值,这个变量在 闭包内部与外部已经没有文法上的联系了,具体表现就是闭包内部修改不影响外面 的变量,外部的修改不影响闭包内部同名的变量。

若加了 __block 修饰,则在文法上面,两者是联系在一起的,闭包内部以及上下 文可以访问到的地方都可以修改,具体的存储地址呢则看闭包是放在栈上还是堆上 了,也就是这个变量实际是跟随闭包的变量。

对了,其实还有全局区的闭包,我觉得只要区别不是栈上的闭包就好了。

14年12月10日 周三 21:47

OpenGL 3.3 under MacOS (2)

终于查明原因了,并不是 GLFW 不堪用的问题,而是需要在 glGenBuffers 之前, 先跑下面:

    glGenVertexArrays(1, &vaoHandle);
    glBindVertexArray(vaoHandle);

当然,那些诸如 <Learning Modern 3D Graphics Programming> 的书上面是没有 这两句代码的。

蛋疼呀。

14年12月9日 周二 19:42

OpenGL 3.3 under MacOS

其实我想说的是在黑苹果下的情况,一般阿婆是只打开 OpenGL 2.1 而已的,用 X11 XQuartz 或者 GLEW,都无法使用 OpenGL 3.3 的,对于我这个初学者来说, 想从 Modern OpenGL 入门,痛苦不堪。

首先,还是先确认黑苹果在硬件上能够支持的 OpenGL API 版本吧,App Store 上 下载 OpenGL Extensions Viewer 来确认硬件情况,比如我的是 GeForce 310,是 能够支持到 OpenGL 3.3 Core 的。

剩下的就是驱动问题了,谷歌得到的结果是建议用新的 GLFW 而不是 GLUT 或者 GLEW 来做,GLFW 在 MacOS 下默认也只打开 OpenGL 2.1 而已,听说要这样才能 打开 3.3 的 Core:

static void using_opengl_version_330(void) {
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
}

设置完之后,我就按教程走了,但是,又出现了意外的情况,在我的黑苹果上面, 330 的 shader 源码都编译链接过了,但就是没有显示,窗口里面都是 黑的。总之 OpenGL 2.1 可以,3.3 就是不行。即便 glClear 颜色显示 OK,loop 循环也有走。

又不甘心使用 OpenGL 2.1 呢,只好救助于 Apple 原生的系统接口看看了,还好 谷歌查到了不错的东西 GLEssentials,是官方提供的 Objective-C 使用 OpenGL 3.3 API 编程的例子。

立马左上角的 "Download Sample Code",XCode 打开工程瞬间编译完成,看了一 下接口以及 Shader,确认是使用 OpenGL 3.3 接口无疑,demo 也挺高大上的,果 然官方的东西就是妥妥的。

之前依靠跨平台各种库、以及过时的库起步,加上黑苹果的原因吧,缓慢起步。

14年12月1日 周一 22:53

实践了一个 JPEG 解码器

为了更好地了解 JPEG 的解码过程,实践了一个 JPEG 解码器,放到个人 github 上面了,http://github.com/lalawue/jpeg_dec.

参考了不少中英文资料,更多的是中文的,marker 的部分倒是看了 itu-81 的图表。

不需要用 shadowsocks 翻墙技能,仅靠百度到的中文博客歪歪斜斜排版杂乱不堪 的文章,其实也能从无到有实践一个 JPEG 解码器。

也参考了不少 nanojpeg 的代码以及处理流程,最复杂的部分,在我看来是霍夫曼 编码的生成,是自己理解得不好,数据结构课上面的霍夫曼编码部分忘得差不多了。

而 nanojpeg 包括 public domain 的 c++ jpegd 实现的霍夫曼码,其生成我感觉 都不明晰,所以花了不少时间。这个部分倒是中文的某些博客写得很清楚,虽然这 些博客其实也是不知道从哪里抄的(未必是博主第一手资料,且有些还无来源)。

实现了霍夫曼表后,接下来的困难是 IDCT 的部分,看了几个解码库的,包括 libjpeg 的,README 里面写得明明白白:

If you think that you know about DCT-based JPEG after reading this book,
then you are in delusion.

所以就不琢磨了,说不好都是优化过的代码,调试出来的,不是给人看的,这部分 直接来自 nanojpeg,是一个整型使用位移调整精度的 IDCT 算法,传说的 AA&N 算法,如 libjpeg 里面的,没有看得很明白,其实 jpgd c++ 用的跟 libjpeg 一 样,有用到浮点就是。

然后是一些细节问题,估计看 ITU-81 里面也难看得明白,比如霍夫曼的 VLC 部 分其实我是根据 nanojpeg 的输出比对排错的、IDCT 的部分也是;还有读到 0xffd9 后补齐 bits 的问题,都补 binary 1;以及每个 component 的 dc restart interval 问题等等。

八卦一下,Mac 下面预览导出的 jpeg 图片,霍夫曼表是一个 DHT segment 带一 个的,PS 则是一个 DHT segment 带多个的。

前人也总结了太多有关 JPEG 结构、解码所需要的知识,这里不重复了,评论下看 到的一些源码吧。

首先目前自己实现的是一个仅支持 Baseline DCT、H1V1 chroma sampling、YCbCr 色彩的 JPEG decoder。其实绝大部分的 JPEG 都是 Baseline、H1V1 的,Gray Scale 其实要比 YCbCr 的简单,再看看后续要不要加上。

nanojpeg 为了实现的简单,资源都全部先申请,包括输出的图片缓冲,亮点是为 了霍夫曼解码的方便,每个霍夫曼表开了个 1^16 条目的数组,将最多 256 个实 际变长码 map 到里面去,这种 LUT 的时间效率没得说了,它自己也介绍说解码时 间上面只比 libjpeg 慢一点点(不支持多线程)。不过空间使用率到了这个地步, 为啥不像下面的解码库一样,对 YUV 转 RGB 也先做 LUT 呢。

而 jpgd c++ 是个 public domain 的解码库,看了一下作者,之前在 Valve 呢, 现在项目合并到了 JPEG encoder 里面去,最后的更新是 2012 年的。其霍夫曼变 长码是将常用的长度小于 8 bits 的 VLC map 到一个 1^8 的空间里面去,剩下的 放到一个 512 大小的空间里面进行二次查询,两个 LUT 加上一个顺序查找的空间 (应该是顺序查找的吧,命名为 tree 的)。

小于 7 bits 的变长码,一次 lookup 就可以定位,超过 7 bits,则需要一个个 bits 来顺序检测并跳转查找了。

jpgd c++ 的最大亮点,在于将 YUV 转 RGB 的部分,做了 LUT。这个空间消耗很 小,比 nanojpeg 丧心病狂的 1^16 LUT 空间少了 N 个数量级,且效率很显著, 特别是当图片变大,像素越来越多的时候。

后续有时间,我也想弄一个稍微改进点的霍夫曼变长码检测方法、以及 YUV 转RGB 的 LUT,目前霍夫曼编码部分是完全按照位数长度从小到大顺序检测的,非常非常 耗时,只是作为一个样例来理解的话,倒还好。