NSOperation 笔记

iOS 下的多线程编程有 NSOperation 和 Grand Central Dispatch(GCD) 两种,简单记一些 NSOperation 的使用注意。

NSOperationQueue 相当于一个操作池,operation 添加进来后会按照 First-In-First-Out(FIFO) 的策略自动执行。operation 一般会添加到应用全局共享的自定义 queue,这样避免阻塞主线程的执行。

一些简单的多线程需求没必要动用 NSOperation 这个大家伙,NSInvocationOperation 就很方便:

NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
                                                                 selector:@selector(anyWork)
                                                                   object:nil];
[operationQueue addOperation:op];

- (void)anyWork
{
    //perform any work in operation
}

NSInvocationOperation 不是很方便共享操作,如果某个操作会在很多地方需要,就可以做个 NSOperation 子类封装:

@implementation CustomOperation

- (void)main
{
    //perform any work in operation
}

@end

这个子类只实现了 main 方法,相较 NSInvocationOperation 方便共享。如果需要对操作做更多细致化的功能,比如状态控制,就需要更加复杂的继承实现,参见 AFURLConnectionOperation,这种情况下不继承 main,而是继承实现 start cancel等,然后通过 KVO 手动控制操作状态的切换。

NSOperation 可以设置依赖,A 操作依赖 B 操作完成后才能做,那么就可以设置 B 为 A 的依赖 [A addDependency:B];.

如果各个操作之间没有依赖关系,但是又需要在全部操作都完成后做一些善后工作,有两个解决方案,一是添加所有操作为善后操作的依赖,这样所有其他操作完成后善后操作才会执行,这个方法较为死板,或者可以用 KVO 监听队列操作数,等操作都完成后队列操作为空的时候做善后工作:

[operationQueue addObserver:self forKeyPath:@"operationCount" options:0 context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == operationQueue && [keyPath isEqualToString:@"operationCount"]) {
        if (operationQueue.operationCount == 0) {
			// any final operation.
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

NSOperation or GCD

NSOperation 和 GCD 都能满足多线程需要,那么该选哪个?When to use NSOperation vs. GCD 的观点:

Always use the highest-level abstraction available to you, and drop down to lower-level abstractions when measurement shows that they are needed.

NSOperation 相比 GCD 提供了更多功能,比如操作执行状态,操作执行的暂停、取消,比如操作之间的依赖,比如控制操作队列同一时间可执行操作的数量。

GCD 相比 NSOperation 使用方便,系统开销小性能好。

实际项目中较为简单的小操作直接 GCD,灵活方便;规模较大控制复杂的操作还是通过 NSOperation 为好,也能享用高级 API 提供的方便。

Semantic Versioning 匹配

Semantic Versioning 直译为语义化版本,格式为 MAJOR.MINOR.PATCH,比如 1.2.3 代表第一主版本第二次版本第三补丁修正版,版本号唯一且可比较,更多信息参考官网介绍。

在 Node.js package.json 或 Bower bower.json 就是用 semver 做版本检查。记个笔记,假设当前最新版是 1.2.5:

  • 1.2.3 明确指定版本,需完全匹配,安装的版本就是 1.2.3.
  • >1.2.3 大于指定版本,匹配符合条件的最新版 1.2.5.
  • <1.2.3 小于指定版本,匹配符合条件的最新版 1.2.2.
  • <=1.2.3 可以包括补丁修正测试版,比如 1.2.3-beta,
  • 1.2.3 - 2.3.4 等同于 >=1.2.3 <=2.3.4.
  • ~1.2.3 接近于 1.2.3 的版本,等同于 >=1.2.3-0 <1.3.0-0,也就是相同主版本,相同次版本,补丁修正大于等于所需版本均符合。这种情况是实际使用中最多的,保持最大的兼容性.
  • ~1.2 等同于 >=1.2.0-0 <1.3.0-0,相同主版本、相同次版本均符合,等于 1.2.x
  • ~1 等同于 >=1.0.0-0 <2.0.0-0,相同主版本均可,等于 1.x.
  • * 任意版本,会匹配当前可用的最新版 1.2.5.
  • 1.2.3 || 1.3.2 哪个满足取哪个,如果两者均符合取第二个。

主要就是 ~ 匹配的使用,一般就用 ~1.2.3,保持次版本内最新,又保持最大兼容性。

参考 semver for npmpackage.json.

Docker 笔记

所有 docker 操作都需要 root 权限,需要加 sudo:

docker search gitlab

搜索服务镜像(image),挑选有 TRUSTED 标示的,相对好一些。

docker pull NAME

下载相应镜像,由于 index.docker.io 被墙,需要梯子,下载会很慢。

docker build -t NAME .

在当前目录根据 Dockerfile 构建容器,--rm 自动删除 build 中间状态的容器。

docker run -i -t -p 8080:80 NAME

运行一个 Container,支持的参数:

  • -d Detached 或者 daemon mode,后台运行。
  • -i -t 开一个 tty 终端,保持交互模式,这两个一般共同使用。
  • -e 设置环境变量参数,参考 Install GitLab With Docker
  • -p [host_port]:[container_port] 映射 HOST 端口到容器,方便外部访问容器内服务,host_port 可以省略,省略表示把 container_port 映射到一个动态端口。
  • -v [host-path]:[container-path] 把 HOST 文件夹挂载到 Container 用以保存数据。
  • --rm 自动删除已运行存在的相同 IMAGE 的容器。
docker attach --sig-proxy=false CONTAINER

attach 后台运行的容器,加上 --sig-proxy=false 参数可以通过 Ctrl+C detach,不然一旦 attach 就没办法取消。

docker commit --run='COMMAND' -m 'message' CONTAINER IMAGE:tag

登录容器做一些修改,退出到 HOST 保存修改到镜像,或者直接在 HOST 通过 --run 给正在运行的容器发送命令并保存到镜像。

docker stop/start/restart/kill/rm CONTAINER

停止、重启、杀死、删除容器。

docker ps -a -q

列出当前运行的容器,-a 会列出所有,包括已停止的,-q 只列出容器 ID。docker ps -a -q | xargs docker rm 可以删除所有未运行的容器。

docker logs -f CONTAINER

查看容器运行日志。

docker cp CONTAINER:/PATH HOSTPATH

拷贝容器内文件或文件夹到 HOST。目前只支持 Container 到 HOST 的单向拷贝,HOST 到 Container 可以通过 insert 命令。

docker insert IMAGE URL PATH

将 URL 文件内容写入相应 PATH,这个操作不修改原来 IMAGE 内容,而是再它的基础上新建一个 IMAGE.

docker images

列出已安装的镜像。可以通过 docker rmi IMAGE 删除镜像。

docker inspect CONTAINER | grep IPAddress

检查容器配置,包含内部 IP 等信息。

更多可参考 Docker 文档

Install GitLab with Docker

GitLab 是个非常不错的 GitHub clone,很适合团队自建 git 服务器。但是由于 GitLab 是个 RoR 应用,加上 gitlab-shell 的权限要求等等,GitLab 的部署甚是麻烦。

Docker 简单说就是基于 LXC 的类 VM 方案,当然比 Virtual Box 等 VM 要高效、省资源,应用和依赖打包成一个容器,很方便部署。

用 Docker 部署 GitLab 首先要找一个可用的镜像(image):

sudo docker search gitlab

选用 sameersbn/gitlab,原因是更新较快,文档详细,支持 -e 设置环境变量,基本上不需要修改安装配置。

省事的话直接 sudo docker pull sameersbn/gitlab 即可,或者:

git clone https://github.com/sameersbn/docker-gitlab.git
cd docker-gitlab
添加 HOST /root/.ssh/id_rsa.pub 到 authorized_keys,这样可以免密码从 HOST 登录 Container。
sudo docker build -t fannheyward/gitlab .

build 完成后启动:

sudo docker run -p 22:22 -d \
  -e "[email protected]" -e "SMTP_PASS=password" \
  -e "[email protected]" -e "[email protected]" \
  -e "GITLAB_SIGNUP=true" \
  -e "GITLAB_HOST=gitlab.host.com" \
  -v /opt/gitlab/data:/home/git/data \
  -v /opt/gitlab/mysql:/var/lib/mysql \
  fannheyward/gitlab
  • -e 用来设置一些环境变量,最少要把 GITLAB_HOST 设置,不然所有项目的 git 地址为 [email protected]
  • -v [host-path]:[container-path] 用来把 HOST 文件夹挂载到 Container 用来保存数据,不然 Container 重启或者关停后数据就会丢失,前面是 HOST 目录,后面是 Container 目录,不要写反。
  • -p 22:22 是把 Container 的 22 端口映射到 HOST 22 端口,HOST 22 改为其他,这样 git ssh 操作的时候方便一些。

在 HOST 上可以通过 ssh 172.17.0.2 登录 Container,IP 地址可以通过 docker inspect c8c2997b9bc4|grep IPAddress 获取。在 Container 里可以做任何修改,安装软件等,修改后在 HOST sudo docker commit c8c2997b9bc4 fannheyward/gitlab:v1 提交保存,这样重启 Container 不会丢失修改。

Done。数据备份和升级参考 sameersbn/gitlab 文档。

Make an iOS Static Library

做一个 iOS 静态库需要注意的东西:

  1. namespace 冲突。静态库用了某第三方库,项目也用了同样的第三方库,在编译的时候就会有 duplicate symbol 错误,因为有两份同样的第三方库。解决办法就是把用到的第三方库加上自定义前缀,包括类名、delegate 协议、常量名,尤其需要注意 Category 的方法名要修改。

  2. 封装静态库的时候应尽量避免引入重量级第三方库,多自己进行封装。

  3. 一个静态库要有自己独有的前缀,所有类名、常量等都要加同样的前缀。

  4. 真机+模拟器支持。Xcode 默认只会用当前环境(真机或模拟器)生成静态库,这样的 SDK 不方便其他项目开发时调试。解决办法就是通过脚本生成一份通用库,build_universal_library.sh,via SO.

  5. 文档。静态库的方便是使用者直接拿你提供的方法来用,无需关注具体实现;不方便在于看不到实现,出现问题无法排查,因此需要把 SDK 的版本、更新历史、使用、FAQ 等写成文档,方便使用,也显得 SDK 比较正式规范。

  6. 图片等资源文件用 bundle 方式打包。一个简单制作 bundle 的方法:新建文件夹,重命名为 YourSDK.bundle,然后 Show Package Contents 打开,加入图片。使用图片的时候需要指明 bundle: [UIImage imageNamed:@"YourSDK.bundle/img.png"]。也可以用 Target 方式制作 bundle,比如 iOS Library With Resources.

  7. 修改 SDK product path,主要是方便打包,参见 build_universal_library.sh.

  8. SDK 头文件加上版本号和简单的使用注释,开发者不太喜欢长篇大论的文档 :D.

  9. 如果 SDK 有用到 Category,注意项目设置 Other Linker Flags 添加 -ObjCQA1490.

  10. 开发时可以把 SDK 用子项目形式加到 SDKDemo 项目下,这样可以边开发边测试。SDKDemo 修改 User Header Search Paths${SYMROOT}/${CONFIGURATION}-universal/YourSDK,路径和 build_universal_library.sh 保持一致。

Grunt serve with Proxy

在用 Grunt 开发时可能需要连接外部服务,比如 grunt serve 服务在 http://127.0.0.1:9000, 当前页需要请求 http://127.0.0.1:9090/api 服务,这时候如果直接请求 /api 就变成了 http://127.0.0.1:9000/api,结果 404,因为这个地址是不存在的;如果直接请求 http://127.0.0.1:9090/api 就会出现跨域问题。

比较方便的解决方案是在 grunt server 层做个代理,把 /api 请求转发到需要的服务。有个现成插件 grunt-connect-proxy 可以直接用。

下载安装:npm install grunt-connect-proxy --save-dev,在 Gruntfile.js 添加 grunt.loadNpmTasks('grunt-connect-proxy'); 启用。修改 connect 段设置,添加 proxies 和 livereload - middleware:

grunt.initConfig({
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      livereload: {
        options: {
          open: true,
          base: [
            '.tmp',
            '<%= yeoman.app %>'
          ],
          middleware: function (connect, options) {
            var middlewares = [];
            var directory = options.directory || options.base[options.base.length - 1];
            if (!Array.isArray(options.base)) {
                options.base = [options.base];
            }
            options.base.forEach(function(base) {
                // Serve static files.
                middlewares.push(connect.static(base));
            });

            // Setup the proxy
            middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest);

            // Make directory browse-able.
            middlewares.push(connect.directory(directory));

            return middlewares;
          }
        }
      },
      proxies: [
        {
          context: '/api',
          host: '127.0.0.1',
          port: 9090,
          https: false,
          changeOrigin: false,
          xforward: false
        }
      ]
    },
});

serve 任务下添加 configureProxies,注意要加在 connect 任务前:

grunt.registerTask('serve', function (target) {
  if (target === 'dist') {
    return grunt.task.run(['build', 'connect:dist:keepalive']);
  }

  grunt.task.run([
    'clean:server',
    'bower-install',
    'concurrent:server',
    'autoprefixer',
    'configureProxies',
    'connect:livereload',
    'watch'
  ]);
});

重启 grunt serve 即可。

Date Timestamp Conversion in Lua

Datetime to Timestamp

local dt = {year=2013, month=12, day=25, hour=0, min=0, sec=0}
print(os.time(dt))

-- 1387900800

Timestamp to Datetime

local ts = os.time()
print(os.date('%Y-%m-%d %H:%M:%S', ts)

-- 2013-12-25 22:09:51

More: Date and Time

匆匆

李剑青 -「匆匆」:

离家时故作轻松 留给娘的是匆匆

那些过时的青春梦
普通得不能再普通
你肯定懂
褪尽了青涩和懵懂
当人在异乡才知感动

[self review:2013];

2013 年度个人总结。先对照去年计划。

工作

iDev 深入,比如 runtime,自动化测试等,尝试一下 OS X 开发。

Fail。

服务端开发学习。

Done.

学一门新语言,Lua/Go。

Done.

生活

学车考驾照。

Done.

健身锻炼。

Fail.

60 及格分。


工作

虎头蛇尾。

按照去年的个人工作计划,今年有意加强了服务端开发,加上团队的支持,取得了一点点成绩,但总体来说今年的工作暴露了自己很多的问题。

  1. 产品能力欠缺。随着规模的扩大,团队有意识的让几个老员工尝试开发+产品管理双轨制,在大家都比较顺利的转型成功干的热火朝天的时候,自己这边却一直没啥起色。主要原因是自己对产品热情不够,缺乏长期的对某个产品的深入思考,给人的感觉就是这个东西只了解了表层,对整体发展方向没有深挖扩展,很少有价值的建设性意见。也许是因为一直以来的项目都有具体产品负责人,也许是因为太偏向技术,总之现在自己的产品能力和大家有很大差距,游离在团队核心边缘。

  2. 带团队的能力不足。作为 leader 对项目进度的把控经常出现偏差,缺乏规划、进度检查和反思,没有及时提早发现问题,往往等出现问题已经是很严重了。另外一方面就是对团队氛围建设的忽视,很多时候只关注自己手头的事,没有调动大家对项目的信心和积极性。这个的原因可能和自己做开发出身比较偏向温和、自觉的做事方式有关,忽视了团队的执行力,在协同工作没有起到引导作用。

  3. 开发以外的沟通较少。不管是项目内还是和其他项目组之间关于产品、运营、测试等等方面的沟通交流都不多,这个完全就是自己原因,产品热情不高,一步慢步步慢。

  4. iOS 开发深度挖掘不够。不仅深度挖掘不够,甚至已有技术都有些生疏。下半年 iOS 7 的发布了解关注了大量的新增特性,但只有理论学习,缺乏实际项目实践,完全是纸上谈兵,更别说 runtime 等深入。iOS 开发能力没有提升,这才是自己最大的危机感。

  5. 服务端技术深度不够。目前自己服务端能力只是应用层问题的解决实现,在出现框架或偏底层问题的时候就没了思路,考虑问题不周全,容易跑偏。

  6. Full-Stack 很容易多个都知道,多个都不精。这就是自己目前的技术现状,接下来要做的就是技术的深学习,基础,体系架构。

  7. 执行力不够。以上问题的原因都可以归结于一点,自己的执行力还很不够,看看 iDevNotes 还有几篇笔记未完成,LearnList 那么多没看,包括 HackingWeek 项目的停滞,出现这些问题也就没啥奇怪了。这才是自己最大的问题。

反思自己的问题很容易,改进才是难点。先从执行力入手,多思考产品,多学习,找准自己在团队中的位置。

生活

2013-10-20,准爸爸。这就是今年生活上最大的变化。

老实讲,在刚升级做爸爸的那一段时间,大概十月底到十一月中上旬,自己还没有做好这种变化的心理准备。突然之间许多事情扑面而来,回家生还是北京生,要不要在北京建档,要办各种证,要不要换房子等等,这些问题被自己的想象放大为很大的压力,感觉就要压垮自己,结果就是那一段时间脾气很不好,很容易上火生气,状态也定然不好,尽管已尽量克制,但还是影响到工作。

本来老婆应该是需要照顾的人,结果却反了过来让老婆开导我。还好我们比较顺利的解决了各个问题,尽我们能力去做,坦然面对。自己的状态也调整过来,做爸爸对男人来说就是一次心智再成熟的过程。


2014

工作

  1. 产品、管理的转型,提高执行力。
  2. 技术深度的挖掘。
  3. 一年一门新语言,Golang.

生活

  1. 迎接宝宝的到来。
  2. 带爸妈和妹妹旅游一次,带他们来北京?

CoreFoundation 和 NSObject 在 ARC 下的转换

CoreFoundation 有自己的引用计数处理方法,在 CF 下如果生成对象的方法中有 create、retain、copy 就表示 CF 会用自己的方式对引用计数加一,这就需要在结束的时候用 CFRelease() 释放。而 ARC 目前只对 NSObject 对象有自动的引用计数处理,所以在 ARC 如果有 CoreFoundation 对象和 NSObject 对象转换就需要用 __bridge, __bridge_transfer, __bridge_retained 进行引用计数管理的转换。

  • __bridge 表示 CF 对象和 NSObject 的引用计数平衡,无需转换管理权。适用于用不包含 create、retain、copy 的方法获取的 CF 对象转换为 NSObject。
  • __bridge_transfer 表示将 CF 对象的引用计数管理员转移到 NSObject 由 ARC 管理,无需再用 CFRelease() 释放。
  • __bridge_retained 表示将 NSObject 对象的引用计数管理权转移到 CF 管理,并且引用计数加一,那么在 CF 层就需要用 CFRelease() 释放该对象。

SDK 有两个宏 CFBridgingRetain, CFBridgingRelease 可以直接用,要注意 CFBridgingRetain 后要用 CFRelease() 释放。

// After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef CFBridgingRetain(id X) {
    return (__bridge_retained CFTypeRef)X;
}

NS_INLINE id CFBridgingRelease(CFTypeRef CF_CONSUMED X) {
    return (__bridge_transfer id)X;
}

参考 ARC工程转换和开发注意事项