更新了 Mac OS X 11后发现,MacVim 不再能够通过Terminal用命令打开了。

mvim hello.txt

于是尝试将 mvim 重新复制到/usr/bin/中去

sudo cp -f mvim /usr/bin/

然而出现了权限问题:

cp: /usr/bin/mvim: Operation not permitted

搜索之后发现,是El Capitan 加入了Rootless机制,不再能够随心所欲的读写很多路径下了。设置 root 权限也不行。

Rootless机制将成为对抗恶意程序的最后防线

于是尝试关闭 Rootless。重启按住 Command+R,进入恢复模式,打开Terminal。

csrutil disable

重启即可。如果要恢复默认,那么

csrutil enable
附录:

csrutil命令参数格式:

csrutil enable [–without kext | fs | debug | dtrace | nvram][–no-internal]

禁用:csrutil disable

(等同于csrutil enable –without kext –without fs –without debug –without dtrace –without nvram)

其中各个开关,意义如下:

  • B0: [kext] 允许加载不受信任的kext(与已被废除的kext-dev-mode=1等效)
  • B1: [fs] 解锁文件系统限制
  • B2: [debug] 允许task_for_pid()调用
  • B3: [n/a] 允许内核调试 (官方的csrutil工具无法设置此位)
  • B4: [internal] Apple内部保留位(csrutil默认会设置此位,实际不会起作用。设置与否均可)
  • B5: [dtrace] 解锁dtrace限制
  • B6: [nvram] 解锁NVRAM限制
  • B7: [n/a] 允许设备配置(新增,具体作用暂时未确定)

link: http://www.jianshu.com/p/22b89f19afd6

几乎每一个 PHP 程序员都发布过代码,可能是通过 ftp 或者 rsync 同步的,也可能是通过 svn 或者 git 更新的。一个活跃的项目可能每天都要发布若干次代码,但是现实却是很少有人注意其中的细节,实际上这里面有好多坑,很可能你就在坑中却浑然不知。

一个正确实现的发布系统至少应该支持原子发布。如果说每一个版本都表示一个独立的状态的话,那么在发布期间,任何一次请求只能在单一状态下被执行。如此称之为支持原子发布;反之如果在发布期间,一次请求跨越不同的状态,那么就不能称之为原子发布。我们不妨举个例子来说明一下:假设一次请求需要 include 两个 PHP 文件,分别是 a.php 和 b.php,当 include a.php 完成后,发布代码,接着 include b.php,如果处理不当的话,那么就可能会导致旧版本的 a.php 和新版本的 b.php 同时存在于同一个请求之中,换句话说就是没有实现原子发布。

开源世界里有很多不错的发布代码工具,比如 ruby 社区的 capistrano,其流程大致就是发布代码到一个全新的目录,然后再软链接到真正的发布目录。

.
├── current -> releases/v1
└── releases
    ├── v1
    │   ├── foo.php
    │   └── bar.php
    └── v2
        ├── foo.php
        └── bar.php

不过鉴于 PHP 本身的特殊性,如果只是简单套用上面的流程,那么将很难实现真正的原子发布。要理清个中缘由,还需要了解一下 PHP 中的两个 Cache 的概念:

  • opcode cache
  • realpath cache

先聊聊 opcode cache,早先大家一直用 apc,现在多半选择 zend opcode,关于它的作用,大家都已经很熟悉,不必多言,唯一需要注意的是 apc 和 zend opcode 对缓存键的选择有所差异:apc 选择的是文件的 inode,zend opcode 选择的是文件的 path。

再聊聊 realpath cache,它的作用是缓冲获取文件信息的 IO 操作,大多数时候它对我们而言是透明的,以至于很多人都不知道它的存在,需要注意的是 realpath cache 是进程级别的,也就是说,每一个 php-fpm 进程都有自己独立的 realpath cache。

相关的技术细节特别琐碎,建议大家仔细阅读如下资料:

在采用软链接发布代码的时候,通常遇到的第一个问题多半是新代码不生效!不管是开启了 apc.stat 或者 opcache.validate_timestamps 配置,还是调用了 apc_clear_cache 或者 opcache_reset 方法,均无效,重启 php-fpm 自然是能够解决问题,不过对脚本语言来说重启太重了!难道除了重启就没有别的办法了么?

事实上之所以会出现这样的问题,主要是因为 opcode cache 是通过 realpath cache 获取文件信息,即便软链接已经指向了新位置,但是如果 realpath cache 里还保存着旧数据的话,opcode cache 依然无法知道新代码的存在,缺省情况下,realpath_cache_ttl 缓存有效期是两分钟,这意味着发布代码后,可能要两分钟才能生效。为了让发布尽快生效,需要以进程为单位清除 realpath cache:

<?php

$key = 'php.pid_' . getmypid();

if (($rev = apc_fetch($key)) != DEPLOY_VERSION) {
    if($rev < DEPLOY_VERSION) {
        apc_store($key, DEPLOY_VERSION);
    }
    
    clearstatcache(true);
}

?>

如此在 apc 环境下基本就能工作了,但是在 zend opcode 环境下还可能有问题。因为在缺省情况下 opcache.revalidate_path 是关闭的,此时会缓存符号链接的值,这会导致即便软链接指向修改了,也永远无法生效,所以在使用 zend opcode 的时候,如果使用了软链接,视情况最好把 opcache.revalidate_path 激活。

分析到这里,我们不妨反思一下:在 PHP 中原子发布之所以是一个棘手的问题,归根结底是因为软链接和缓存之间的的矛盾。不管是 opcode cache 还是 realpath cache,都是 PHP 固有的缓存特性,基于客观需要无法绕开,如此说来是否有办法绕开软链接,使其成为马奇诺防线呢?答案是 nginx 的 $realpath_root

fastcgi_param SCRIPT_FILENAME $realpath_root $fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

有了 $realpath_root,即便 DOCUMENT_ROOT 目录中含有软链接,nginx 也会把软链接指向的真正的路径发给 PHP,也就是说,对 PHP 而言,软链接已经不存在了!不过作为代价,每一次请求,nginx 都要通过相对昂贵的 IO 操作获取 $realpath_root 的值,通过 strace 命令我们能监控这一过程,下图从 current 到 foo 的过程:

realpath

在本例中,压测发现使用 $realpath_root 后,性能下降了大约 5% 左右,不过明眼人一下就能发现,虽然 $realpath_root 导致了 lstat 和 readlink 操作,但是 lstat 操作的次数是和目录深度成正比的,也就是说目录越深,执行的 lstat 次数越多,性能下降也就越大。如果能够降低发布目录的深度,那么可以预计性能下降能够控制在 1% 左右。

结尾介绍一下 Deployer,它是 PHP 中做得比较好的工具,有很多特色,比如支持并行发布,具体演示如下图,左边是串行,右边是并行,使用「vvv」能得到更详细信息:

deploy

不过 Deployer 在原子发布上有一点瑕疵,具体见 deploy:release 代码:

<?php

run("cd {{deploy_path}} && if [ -h release ]; then rm release; fi");
run("ln -s $releasePath {{deploy_path}}/release");

?>

也就是说,在切换软链接的时候,它是先删除再创建,是一个两步操作,理论上如果有请求在这两步中间进入的话,那么将会出现找不到文件的错误。

问题到这里,大部分人会觉得使用「ln -sfn」就好了,实际上也是错误的:

shell> strace ln -sfn releases/foo current
symlink("releases/foo", "current")      = -1 EEXIST (File exists)
unlink("current")                       = 0
symlink("releases/foo", "current")      = 0

通过 strace 我们能清晰的看到,虽然表面上使用「ln -sfn」是一步操作,但是内部依然是按照先删除再创建的逻辑执行的,实际上这里应该搭配使用「ln & mv」:

shell> ln -sfn releases/foo current.tmp
shell> mv -fT current.tmp current

先通过 ln 创建一个临时的软链接,再通过 mv 实现原子操作,此时如果使用 strace 监控,会发现 mv 的「T」选项实际上仅仅执行了一个 rename 操作,所以是原子的。

link: http://huoding.com/2016/05/27/515

首先从整体上介绍git服务器的工作原理:多个客户端,其中可以包括仓库管理员,通过将自己的ssh公钥上传到服务器仓库keydir目录,统一调用Git专用账号git进行访问git仓库,不同的用户可以根据不同的ssh公钥校验登录,进行项目版本的各种操作,包括clone,commit,push,pull等等。
    下面具体介绍Git服务器的搭建,机器环境:ubuntu12.04,Git服务器软件采用gitosis(https://github.com/res0nat0r/gitosis ,网上很多文章给的地址根本无法使用,都是copy的,不给力啊! )。
一、软件安装
    1.1 安装ssh的服务端和客户端:sudo apt-get install openssh-server openssh-client
    1.2 安装git-core软件,这个是git服务的基础:sudo apt-get install git-core
    1.3 安装 Gitosis,这个是git服务器软件

de>sudo apt-get install python-setuptoolsde>de>
de>de>cd /tmp
de>de>git clone https://github.com/res0nat0r/gitosis.git
de>de>
de>de>cd gitosisde>

sudo python setup.py install 

 

    ok,软件的安装都此结束。
 
二、创建Git专用账号git
    sudo useradd -m -s /bin/bash git    //创建git账号,用户家目录默认为/home/git,shell为/bin/bash
    sudo passwd git    //设置git用户的密码
 
三、初始化Git仓库
    1、初始化Git仓库需要一个管理员账号,如上图所示,管理员也是一个客户端用户,所以需要在客户端主机(我的客户端机器是Win7系统)生成一个用户,并且生成ssh-key。具体操作如下:
        
        在客户端安装ssh服务,包括客户端和服务端。如果你的客户端系统是:
            (1)Windows机器,建议直接安装git bash软件,其中包括了ssh这个服务;
            (2)Linux系统,可以按照先前的命令apt-get install openssh-server openssh-client安装ssh服务。
        
        在客户端机器我的账号是huixiao200068,在用户家目录中生成ssh-key:
            (1)Windows机器,打开git bash软件,输入命令
                        cd ~
                        ssh-keygen -t rsa
            (2)Linux系统,也执行以上相同的命令,即可以生成当前用户的ssh-key。
            执行完成之后,输入命令:ls -al,如果出现了.ssh目录,则表示ssh-key成功创建。
    2、把ssh的公钥上传到服务器,我这里假设上传到服务器的临时目录/tmp。
        scp  ~/.ssh/id_rsa.pub git@server:/tmp    //命令中的server改成你自己服务器的IP
    到此为止,仅仅只是做好了初始化仓库的准备工作,上面的2步操作都是在客户端操作的。下面是关键的第三步——初始化,该步骤在服务器端执行。
 
    3、初始化
        sudo -H -u git gitosis-init < /tmp/id_rsa.pub    //将git仓库目录初始化到了git用户家目录下
        此时,git用户的home目录中将出现repositories目录,该目录为git的仓库。
        修改目录权限:sudo chmod 755 /home/git/repositories/gitosis-admin.git/hooks/post-update,该步骤尚不清楚其具体的作用,修改了这个目录的权限有啥用呢?望各位指教。
恭喜你,到此为止,你已经创建了一个git仓库,而且账号huixiao200068是git仓库的管理员啦。下面要做的就是仓库配置啦,管理项目和用户。
四、下载仓库配置项目gitosis-admin到本地客户端
        因为git仓库的配置文件都是以git方式来管理的,所以你需要先下载一份到客户端本地 。
        在你的用户目录下面创建一个临时目录work,
        然后 进入到该目录:cd work,
        然后执行命令:
        git clone git@server:gitosis-admin.git    // 命令中的server改成你自己服务器的IP
        执行完成之后,work目录下会生成gitosis-admin目录,目录下面有一个gitosis.conf文件和一个keydir目录,它们将是下面配置任务的主要操作对象,请牢记它们的位置。
五、新建项目
       1、修改配置文件gitosis.conf,增加如下内容。
            [group first-pro]    //用户组名
            members = huixiao200068    //成员名,多个成员可以用空格隔开
            writable = first-pro    //项目名及其用户对于此项目的权限,目前是可写
        2、创建项目目录 mkdir first-pro
             初始化该目录 cd first-pro;    git init
             添加远程仓库 git remote add origin git@SERVER:first-pro.git
             创建工程文件 touch 1.txt
             添加工程文件到本地仓库中 git add ./1.txt
             提交整个项目到本地仓库 git commit -m “comment”
             提交整个项目到远程仓库 git push origin master    //只有这样子,团队其他开发人员才能看到你修改的代码
             查看提交日志 git log
             查看本地库当前的状态,比如是否有文件需要提交。 git status
        新的项目仓库已经生成,其他开发人员可以git clone 命令从服务器上下载一份工程到自己的本地机器上,协同开发啦!
 
六、新建用户

        关于上一节最后提到的内容,“其他开发人员”,他们是需要管理员来增加和配置的,这一节主要讲怎么添加用户。

(1)客户端操作:
        首先要生成ssh-key,方法和上述说明的一样。
        cd ~
        ssh-keygen -t rsa
        然后一直回车,就OK。然后将生成的id_rsa.pub文件传给GIT服务器管理员
 
(2)服务器端操作:
        管理员将客户上传的id_rsa.pub文件移到gitosis-admin/keydir目录中,并且改名为CLIENT_NAME.pub。注意:如果客户端如下图所示
        ,则CLIENT_NAME为sean@bogon,否则后续操作会出现“Repository read access denied”的错误。
         给项目first-pro增加新的开发者,编辑gitosis.conf文件,vi gitosis.conf。
          [group first-pro]    //用户组名
          members = huixiao200068 sean@bogon    //成员名,多个成员可以用空格隔开
          writable = first-pro    //项目名及其用户对于此项目的权限,目前是可写
         提交修改的管理文件:
            git commit -a -m “add user sean@bogon”
            git push origin master
完成上述2步之后,即可以使用该账号共同开发项目first-pro啦!
        cd ~
        git clone git@SERVER:first-pro.git    //克隆项目到本地
        ……    //do anything you want to do
        commit -am “comment”
        commit push origin master
link: http://blog.163.com/xiaohui_1123@126/blog/static/398052402012102751349705/

在默认拥有的腾讯云服务器是没有额外的数据盘的,默认Linux只有8GB系统盘,一般的网站也足够使用,如果额外购买的数据盘安装系统之后根据不同的面板、系统的路径问题可能不会自动加载到指定的数据盘目录,需要我们手工进行加载数据盘 ,也就是我们常说的挂载。

第一、检查硬盘设备是否有数据盘

当然,在写这篇文章的时候,我是知道有数据盘的,但有些时候我们购买的VPS,默认比如30GB,可能还有20GB没有挂载,所以也需要类似这样的操作先检查一遍。

fdisk -l

检查腾讯云服务器数据硬盘

我们可以看到有268GB的数据盘没有挂载,看好前面的路径/dev/vdb

第二、数据硬盘分区

fdisk /dev/vdb

依次输入 n 、p、 1、 回车、回车、wq

这里的VDB是我们上面看到数据硬盘的名称,如果你不是这个需要根据你真实的盘名称替换,如果是和我一样,那就直接复制。

第三、ext3格式化分区

mkfs.ext3 /dev/vdb1

第四、挂载新分区

A – 新建目录

mkdir /home

因为AMH面板是安装在HOME目录的,所以我们需要新建目录,如果是WDCP面板,我们应该知道是WWW目录。

B – 挂载分区

mount /dev/vdb1 /home

第五、写入fstab 设置开机自动挂载

echo ‘/dev/vdb1 /home ext3 defaults 0 0’ >> /etc/fstab

第六、检查是否挂载成功(df -h )

这里我们可以看到247GB(250GB)已经挂载完成,在HOME目录中。

 

link: http://www.laozuo.org/4888.html

三. 常用方法的封装

虽然 PhotoKit 的功能强大很多,但基于兼容 iOS 8.0 以下版本的考虑,暂时可能仍无法抛弃 ALAssetLibrary,这时候一个比较好的方案是基于 ALAssetLibrary 和 PhotoKit 封装出一系列模拟系统 Asset 类的自定义类,然后在其中封装好兼容 ALAssetLibrary 和 PhotoKit 的方法。

这里列举了四种常用的封装好的方法:原图,缩略图,预览图,方向,下面直接上代码,代码中有相关注释解释其中的要点。其中下面的代码中常常出现的 [[QMUIAssetsManager sharedInstance] phCachingImageManager] 是 QMUI 框架中封装的类以及单例方法,表示产生一个 PHCachingImageManager 的单例,这样做的好处是 PHCachingImageManager 需要占用较多的资源,因此使用单例可以避免无谓的资源消耗,另外请求图像等方法需要基于用一个 PHCachingImageManager 实例才能进行进度续传,管理请求等操作。

1. 原图

由于原图的尺寸通常会比较大,因此建议使用异步拉取,但这里仍同时列举同步拉取的方法。这里需要留意如前文中所述,ALAssetRepresentation 中获取原图的接口 fullResolutionImage 所得到的图像并没有带上系统相册“编辑”(选中,滤镜等)的效果,需要额外获取这些效果并手工叠加到图像上。

.h 文件

/// Asset 的原图(包含系统相册“编辑”功能处理后的效果)
– (UIImage *)originImage;

/**
*  异步请求 Asset 的原图,包含了系统照片“编辑”功能处理后的效果(剪裁,旋转和滤镜等),可能会有网络请求
*
*  @param completion        完成请求后调用的 block,参数中包含了请求的原图以及图片信息,在 iOS 8.0 或以上版本中,
*                           这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
*                           获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil。
*  @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
*  @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil。
*
*  @return 返回请求图片的请求 id
*/
– (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

.m 文件

– (UIImage *)originImage {
if (_originImage) {
return _originImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeDefault
options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullResolutionImageRef = [_alAssetRepresentation fullResolutionImage];
// 通过 fullResolutionImage 获取到的的高清图实际上并不带上在照片应用中使用“编辑”处理的效果,需要额外在 AlAssetRepresentation 中获取这些信息
NSString *adjustment = [[_alAssetRepresentation metadata] objectForKey:@”AdjustmentXMP”];
if (adjustment) {
// 如果有在照片应用中使用“编辑”效果,则需要获取这些编辑后的滤镜,手工叠加到原图中
NSData *xmpData = [adjustment dataUsingEncoding:NSUTF8StringEncoding];
CIImage *tempImage = [CIImage imageWithCGImage:fullResolutionImageRef];

NSError *error;
NSArray *filterArray = [CIFilter filterArrayFromSerializedXMP:xmpData
inputImageExtent:tempImage.extent
error:&error];
CIContext *context = [CIContext contextWithOptions:nil];
if (filterArray && !error) {
for (CIFilter *filter in filterArray) {
[filter setValue:tempImage forKey:kCIInputImageKey];
tempImage = [filter outputImage];
}
fullResolutionImageRef = [context createCGImage:tempImage fromRect:[tempImage extent]];
}
}
// 生成最终返回的 UIImage,同时把图片的 orientation 也补充上去
resultImage = [UIImage imageWithCGImage:fullResolutionImageRef scale:[_alAssetRepresentation scale] orientation:(UIImageOrientation)[_alAssetRepresentation orientation]];
}
_originImage = resultImage;
return resultImage;
}

– (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_originImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_originImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _originImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_originImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self originImage], nil);
}
return 0;
}
}

 2. 缩略图

相对于在拉取原图时 ALAssetLibrary 的部分需要手工叠加系统相册的“编辑”效果,拉取缩略图则简单一些,因为系统接口拉取到的缩略图已经带上“编辑”的效果了。

.h 文件

/**
* Asset 的缩略图
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
*
* @return Asset 的缩略图
*/
– (UIImage *)thumbnailWithSize:(CGSize)size;

/**
* 异步请求 Asset 的缩略图,不会产生网络请求
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
* @param completion 完成请求后调用的 block,参数中包含了请求的缩略图以及图片信息,在 iOS 8.0 或以上版本中,这个 block 会被多次调用,
* 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,获取到高清图后 QMUIAsset 会缓存起这张高清图,
* 这时 block 中的第二个参数(图片信息)返回的为 nil。
*
* @return 返回请求图片的请求 id
*/
– (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion;

.m 文件

– (UIImage *)thumbnailWithSize:(CGSize)size {
if (_thumbnailImage) {
return _thumbnailImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale)
contentMode:PHImageContentModeAspectFill options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef thumbnailImageRef = [_alAsset thumbnail];
if (thumbnailImageRef) {
resultImage = [UIImage imageWithCGImage:thumbnailImageRef];
}
}
_thumbnailImage = resultImage;
return resultImage;
}

– (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion {
if (_usePhotoKit) {
if (_thumbnailImage) {
if (completion) {
completion(_thumbnailImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _thumbnailImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_thumbnailImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self thumbnailWithSize:size], nil);
}
return 0;
}
}

 3. 预览图

与上面的方法类似,不再展开说明。

.h 文件

/**
*  Asset 的预览图
*
*  @warning 仿照 ALAssetsLibrary 的做法输出与当前设备屏幕大小相同尺寸的图片,如果图片原图小于当前设备屏幕的尺寸,则只输出原图大小的图片
*  @return Asset 的全屏图
*/
– (UIImage *)previewImage;

/**
*  异步请求 Asset 的预览图,可能会有网络请求
*
*  @param completion        完成请求后调用的 block,参数中包含了请求的预览图以及图片信息,在 iOS 8.0 或以上版本中,
*                           这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
*                           获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil。
*  @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
*  @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil。
*
*  @return 返回请求图片的请求 id
*/
– (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

.m 文件

– (UIImage *)previewImage {
if (_previewImage) {
return _previewImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT)
contentMode:PHImageContentModeAspectFill
options:imageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullScreenImageRef = [_alAssetRepresentation fullScreenImage];
resultImage = [UIImage imageWithCGImage:fullScreenImageRef];
}
_previewImage = resultImage;
return resultImage;
}

– (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_previewImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_previewImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _previewImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_previewImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self previewImage], nil);
}
return 0;
}
}

 4. 方向(imageOrientation)

比较奇怪的是,无论在 PhotoKit 或者是 ALAssetLibrary 中,要想获取到准确的图像方向,只能通过某些 key 检索所得。

.h 文件

– (UIImageOrientation)imageOrientation;

.m 文件

- (UIImageOrientation)imageOrientation {
    UIImageOrientation orientation;
    if (_usePhotoKit) {
        if (!_phAssetInfo) {
            // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取
            [self requestPhAssetInfo];
        }
        // 从 PhAssetInfo 中获取 UIImageOrientation 对应的字段
        orientation = (UIImageOrientation)[_phAssetInfo[@"orientation"] integerValue];
    } else {
        orientation = (UIImageOrientation)[[_alAsset valueForProperty:@"ALAssetPropertyOrientation"] integerValue];
    }
    return orientation;
}

系列文章:

一. 概况

本文接着 iOS 开发之照片框架详解,侧重介绍在前文中简单介绍过的 PhotoKit 及其与 ALAssetLibrary 的差异,以及如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法。

这里引用一下前文中对 PhotoKit 基本构成的介绍:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHAssetCollection: PHCollection 的子类,表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHFetchResult: 表示一系列的资源结果集合,也可以是相册的集合,从 PHCollection 的类方法中获得
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

这里还有一个额外的概念 PHCollectionList,表示一组 PHCollection,它本身也是一个 PHCollection,因此 PHCollection 作为一个集合,可以包含其他集合,这使到 PhotoKit 的组成比 ALAssetLibrary 要复杂一些。另外与 ALAssetLibrary 相似,一个 PHAsset 可以同时属于多个不同的 PHAssetCollection,最常见的例子就是刚刚拍摄的照片,至少同时属于“最近添加”、“相机胶卷”以及“照片 – 精选”这三个 PHAssetCollection。关于这几个概念的关系如下图:

二.  PhotoKit 的机制

1. 获取资源

在 ALAssetLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据,并且数据是从 ALAssetLibrary(照片库) – ALAssetGroup(相册)- ALAsset(资源)这一路径逐层获取,即使有直接从 ALAssetLibrary 这一层获取 ALAsset 的接口,本质上也是枚举 ALAssetLibrary 所得,并不是直接获取,这样的好处很明显,就是非常符合实际应用中资源的显示路径:照片库 – 相册 – 图片或视频,但由于采用枚举的方式获取资源,效率低而且不灵活。

而在 PhotoKit 中,则是采用“获取”的方式拉取资源,这些获取的手段,都是一系列形如 class func fetchXXX(…, options: PHFetchOptions) -> PHFetchResult 的类方法,具体使用哪个类方法,则视乎需要获取的是相册、时刻还是资源,这类方法中的 option 充当了过滤器的作用,可以过滤相册的类型,日期,名称等,从而直接获取对应的资源而不需要枚举。例如在前文中列举个的几个小例子:

// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@”creationDate” ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

如前面提到过的那样,从 PHAssetCollection 获取中获取到的可以是相册也可以是资源,但无论是哪种内容,都统一使用 PHFetchResult 对象封装起来,因此虽然 PHAssetCollection 获取到的结果可能是多样的,但通过 PHFetchResult 就可以使用统一的方法去处理这些内容(即遍历 PHFetchResult)。例如扩展上面的例子:

// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 这时 smartAlbums 中保存的应该是各个智能相册对应的 PHAssetCollection
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个相册(PHAssetCollection)
PHCollection *collection = fetchResult[i];
if ([collection isKindOfClass:[PHAssetCollection class]]) {
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
// 从每一个智能相册中获取到的 PHFetchResult 中包含的才是真正的资源(PHAsset)
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];
else {
NSAssert(NO, @”Fetch collection not PHCollection: %@”, collection);
}
}// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@”creationDate” ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 这时 assetsFetchResults 中包含的,应该就是各个资源(PHAsset)
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个资源(PHAsset)
PHAsset *asset = fetchResult[i];
}

 2. 获取图像的方式与坑点

经过了上面的步骤,已经可以了解到如何在 PhotoKit 中获取到代表资源的 PHAsset 了,但与 ALAssetLibrary 中从 ALAsset 中直接获取图像的方式不同,PhotoKit 无法直接从 PHAsset 的实例中获取图像,而是引入了一个管理器 PHImageManager 获取图像。PHImageManager 是通过请求的方式拉取图像,并可以控制请求得到的图像的尺寸、剪裁方式、质量,缓存以及请求本身的管理(发出请求、取消请求)等。而请求图像的方法是  PHImageManager 的一个实例方法:

1
2

这个方法中的参数坑点不少,下面逐个参数列举一下其作用及坑点:

  • asset,图像对应的 PHAsset。
  • targetSize,需要获取的图像的尺寸,如果输入的尺寸大于资源原图的尺寸,则只返回原图。需要注意在 PHImageManager 中,所有的尺寸都是用 Pixel 作为单位(Note that all sizes are in pixels),因此这里想要获得正确大小的图像,需要把输入的尺寸转换为 Pixel如果需要返回原图尺寸,可以传入 PhotoKit 中预先定义好的常量 PHImageManagerMaximumSize,表示返回可选范围内的最大的尺寸,即原图尺寸。
  • contentMode,图像的剪裁方式,与 UIView 的 contentMode 参数相似,控制照片应该以按比例缩放还是按比例填充的方式放到最终展示的容器内。注意如果 targetSize 传入 PHImageManagerMaximumSize,则 contentMode 无论传入什么值都会被视为 PHImageContentModeDefault
  • options,一个 PHImageRequestOptions 的实例,可以控制的内容相当丰富,包括图像的质量、版本,也会有参数控制图像的剪裁,下面再展开说明。
  • resultHandler,请求结束后被调用的 block,返回一个包含资源对于图像的 UIImage 和包含图像信息的一个 NSDictionary,在整个请求的周期中,这个 block 可能会被多次调用,关于这点连同options 参数在下面展开说明。

(1)PHImageRequestOptions 与 iCloud 照片库

PHImageRequestOptions 中包含了一系列控制请求图像的属性。

resizeMode 属性控制图像的剪裁,不知道为什么 PhotoKit 会在请求图像方法(requestImageForAsset)中已经有控制图像剪裁的参数后(contentMode),还在 options 中加入控制剪裁的属性,但如果两个地方所控制的剪裁结果有所冲突,PhotoKit 会以 resizeMode 的结果为准。另外,resizeMode 也有控制图像质量的作用。如 resizeMode 设置为 PHImageRequestOptionsResizeModeExact 则返回图像必须和目标大小相匹配,并且图像质量也为高质量图像,而设置为 PHImageRequestOptionsResizeModeFast 则请求的效率更高,但返回的图像可能和目标大小不一样并且质量较低。

在 PhotoKit 中,对 iCloud 照片库有很好的支持,如果用户开启了 iCloud 照片库,并且选择了“优化 iPhone/iPad 储存空间”,或者选择了“下载并保留原件”但原件还没有加载好的时候,PhotoKit 也会预先拿到这些非本地图像的 PHAsset,但是由于本地并没有原图,所以如果产生了请求高清图的请求,PHotoKit 会尝试从 iCloud 下载图片,而这个行为最终的表现,会被 PHImageRequestOptions 中的值所影响。PHImageRequestOptions 中常常会用的几个属性如下:

networkAccessAllowed 参数控制是否允许网络请求,默认为 NO,如果不允许网络请求,那么就没有然后了,当然也拉取不到 iCloud 的图像原件。deliveryMode 则用于控制请求的图片质量。synchronous 控制是否为同步请求,默认为 NO,如果 synchronous 为 YES,即同步请求时,deliveryMode 会被视为 PHImageRequestOptionsDeliveryModeHighQualityFormat,即自动返回高质量的图片,因此不建议使用同步请求,否则如果界面需要等待返回的图像才能进一步作出反应,则反应时长会很长。

还有一个与 iCloud 密切相关的属性 progressHandler,当图像需要从 iCloud 下载时,这个 block 会被自动调用,block 中会返回图像下载的进度,图像的信息,出错信息。开发者可以利用这些信息反馈给用户当前图像的下载进度以及状况,但需要注意 progressHandler 不在主线程上执行,因此在其中需要操作 UI,则需要手工放到主线程执行。

上面有提到,requestImageForAsset 中的参数 resultHandler 可能会被多次调用,这种情况就是图像需要从 iCloud 中下载的情况。在 requestImageForAsset 返回的内容中,一开始的那一次请求中会返回一个小尺寸的图像版本,当高清图像还在下载时,开发者可以首先给用户展示这个低清的图像版本,然后 block 在多次调用后,最终会返回高清的原图。至于当前返回的图像是哪个版本的图像,可以通过 block 返回的 NSDictionary info 中获知,PHImageResultIsDegradedKey 表示当前返回的 UIImage 是低清图。如果需要判断是否已经获得高清图,可以这样判断:

// 排除取消,错误,低清图三种情况,即已经获取到了高清图
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];

另外,当我们使用 requestImageForAsset 发出对图像的请求时,如果在同一个 PHImageManager 中同时对同一个资源发出图像请求,请求的进度是可以共享的,因此我们可以利用这个特性,把 PHImageManager 以单例的形式使用,这样在切换界面时也不用担心无法传递图像的下载进度。例如,在图像的列表页面触发了下载图像,当我们离开列表页面进入预览大图界面时,并不用担心会重新图像会重新下载,只要没有手工取消图像下载,进入预览大图界面下载图像会自动继续从上次的进度下载图像。

如果希望取消下载图像,则可以使用 PHImageManager 的  cancelImageRequest 方法,它传入的是请求图像的请求 ID,这个 ID 可以从 requestImageForAsset 的返回值中获得,也可以从前面提到的包含图像信息的 NSDictionary info 中获得,当然前提是这个这个接收取消请求的 PHImageManager 与刚刚发出请求的 PHImageManager 是同一个实例,如上面所述使用单例是最为简单有效的方式。

最后,还要介绍一个 PHImageRequestOptions 的属性 versions,这个属性是指获取的图像是否需要包含系统相册“编辑”功能处理过的信息(如滤镜,旋转等),这一点比 ALAssetLibrary 要灵活很多,ALAssetLibrary 中并不能灵活地控制获取的图像是否带有“编辑”处理过的效果,例如在 ALAsset 中获取原图的接口 fullResolutionImage 获取到的是不带“编辑”效果的图像,要想获取带有“编辑”效果的图像,只能自行处理获取这些滤镜效果,并手工叠加上去。在我们的 UI 框架 QMUI 中就有对获取原图作出这样的封装,整个过程也较为繁琐,而框架中处理 PhotoKit 的部分则灵活很多,这也体现了 PhotoKit 相比 ALAssetLibrary 的最主要特点——复杂但灵活。文章的第三部分也会详细列出如何处理这个问题。

(2)获取图像的优化

PHImageManager 提供了一个子类 PHImageCachingManager 用于处理图像的缓存,但是这个子类并不只是图像本身的缓存,而是更加实用——处理图像的整个加载过程的缓存。例如要在一个 collectionView 上展示图像列表这类大量的资源图像的缩略图时,可以利用 PHImageCachingManager 预先将一些图像加载到内存中,这对优化 collectionView 滚动时的表现很有帮助。然而,这只是官方说法,实际上由于加载图像的过程并不确定,每个业务加载图像的实际需求都可能不一样,因此 PHImageCachingManager 也采用比较松散的方法去控制这些缓存,其中的关键方法:

- (void)startCachingImagesForAssets:(NSArray<PHAsset *> *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;

需要传入一组 PHAsset,以及 targetSize,contentMode,以及一个 PHImageRequestOptions,如上面所述,这些参数之间的有着互相影响的作用,因此实际上不同的场景对于每个参数要求都不一样,而这些参数的最佳取值也只能通过实际在场景中测试所得。因此,比起使用 PHImageCachingManager,我总结了一些更为简易可行的缓存方法:

  • 获取图片时尽量获取预览图,不要直接显示原件,建议获取与设备屏幕同样大小的图像即可,实际上系统相册预览大图时使用的也是预览图,这也是系统相册加载速度快的原因。
  • 获取图片使用异步请求,如上面所述,当请求为异步时返回图像的 block 会被多次调用,先返回低清图,再返回高清图,这样一来可以大大减少 UI 的等待时间。
  • 获取到高清图后可以缓存下来,简单地使用变量缓存即可,尽量在获取到高清图后避免再次发起请求获取图像。因为即使图像原件已经下载下来,重新请求高清图时因为图片的尺寸比较大,因此系统生成图像和剪裁图像也会花费一些时间。
  • 预先加载图像,如像预览大图这类情景中,用户同时只会看到一张大图,因此在观看某一张图片时,预先请求其邻近两张图片,对于加快 UI 的响应很有帮助。

经过实际测试,如果请求的是缩略图(即尺寸小的图像),那么即使请求的图像很多,仍不会产生任何不流畅的表现,但如果请求的是高清大图,那么即使只是同时请求几张图都会产生不流畅的状况。如上面提到过的那样,这些的状况的出现很可能是请求大图时由图片元数据产生图像,以及剪裁图像的过程耗时较多。所以按实际表现来看,即使 PhotoKit 有自己的缓存策略,仍然很难避免这部分耗时。因此上面几点优化获取图像的策略重点也是放在减少图像大小,异步请求以及做缓存几个方面。

 

link: http://kayosite.com/ios-development-and-detail-of-photo-framework-part-two.html

PhotoKit 是一套比 AssetsLibrary 更完整也更高效的库,对资源的处理跟 AssetsLibrary 也有很大的不同。

首先简单介绍几个概念:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHFetchResult: 表示一系列的资源集合,也可以是相册的集合
  • PHAssetCollection: 表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

下图中 UITableView 的第二个 section 就是 PhotoKit 所列出的所有智能相册

再列出几个代码片段,展示如何获取相册以及某个相册下资源的代码:

// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];

// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];

// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@”creationDate” ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

// 在资源的集合中获取第一个集合,并获取其中的图片
PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
PHAsset *asset = assetsFetchResults[0];
[imageManager requestImageForAsset:asset
targetSize:SomeSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {

// 得到一张 UIImage,展示到界面上

}];

结合上面几个代码片段上看,PhotoKit 相对 AssetsLibrary 主要有三点重要的改进:

  • 从 AssetsLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据。而 PhotoKit 则是通过传入参数,直接获取相应的数据,因而效率会提高不少。
  • 在 AssetsLibrary 中,相册和资源是对应不同的对象(ALAssetGroup 和 ALAsset),因此获取相册和获取资源是两个完全没有关联的接口。而 PhotoKit 中则有 PHFetchResult 这个可以统一储存相册或资源的对象,因此处理相册和资源时也会比较方便。
  • PhotoKit 返回资源结果时,同时返回了资源的元数据,获取元数据在 AssetsLibrary 中是很难办到的一件事。同时通过 PHAsset,开发者还能直接获取资源是否被收藏(favorite)和隐藏(hidden),拍摄图片时是否开启了 HDR 或全景模式,甚至能通过一张连拍图片获取到连拍图片中的其他图片。这也是文章开头说的,PhotoKit 能更好地与设备照片库接入的一个重要因素。

 

link: http://kayosite.com/ios-development-and-detail-of-photo-framework.html

通过PHAssetCollection的以下方法来获取指定的相册:

func fetchAssetCollectionsWithType(_ type: PHAssetCollectionType, subtype subtype: PHAssetCollectionSubtype, options options: PHFetchOptions?) -> PHFetchResult

这个方法需要至少指定两个参数:

enum PHAssetCollectionType : Int {
    case Album //从 iTunes 同步来的相册,以及用户在 Photos 中自己建立的相册
    case SmartAlbum //经由相机得来的相册
    case Moment //Photos 为我们自动生成的时间分组的相册
}

enum PHAssetCollectionSubtype : Int {
    case AlbumRegular //用户在 Photos 中创建的相册,也就是我所谓的逻辑相册
    case AlbumSyncedEvent //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步过来的事件。然而,在iTunes 12 以及iOS 9.0 beta4上,选用该类型没法获取同步的事件相册,而必须使用AlbumSyncedAlbum。
    case AlbumSyncedFaces //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步的人物相册。
    case AlbumSyncedAlbum //做了 AlbumSyncedEvent 应该做的事
    case AlbumImported //从相机或是外部存储导入的相册,完全没有这方面的使用经验,没法验证。
    case AlbumMyPhotoStream //用户的 iCloud 照片流
    case AlbumCloudShared //用户使用 iCloud 共享的相册
    case SmartAlbumGeneric //文档解释为非特殊类型的相册,主要包括从 iPhoto 同步过来的相册。由于本人的 iPhoto 已被 Photos 替代,无法验证。不过,在我的 iPad mini 上是无法获取的,而下面类型的相册,尽管没有包含照片或视频,但能够获取到。
    case SmartAlbumPanoramas //相机拍摄的全景照片
    case SmartAlbumVideos //相机拍摄的视频
    case SmartAlbumFavorites //收藏文件夹
    case SmartAlbumTimelapses //延时视频文件夹,同时也会出现在视频文件夹中
    case SmartAlbumAllHidden //包含隐藏照片或视频的文件夹
    case SmartAlbumRecentlyAdded //相机近期拍摄的照片或视频
    case SmartAlbumBursts //连拍模式拍摄的照片,在 iPad mini 上按住快门不放就可以了,但是照片依然没有存放在这个文件夹下,而是在相机相册里。
    case SmartAlbumSlomoVideos //Slomo 是 slow motion 的缩写,高速摄影慢动作解析,在该模式下,iOS 设备以120帧拍摄。不过我的 iPad mini 不支持,没法验证。
    case SmartAlbumUserLibrary //这个命名最神奇了,就是相机相册,所有相机拍摄的照片或视频都会出现在该相册中,而且使用其他应用保存的照片也会出现在这里。
    case Any //包含所有类型
}
  1. 钩子 “post-commit” 失败(退出代码 255) 没有输出

这是因post-commit脚本文件的权限不对,post-commit 脚本必须有 +x 权限

chmod +x  post-commit

2. web service中调用svn update失败的问题

执行 shell 指令前要加上 export LANG=C.UTF-8 的环境声明,不然 SVN update 时遇到中文会出现 error,Ubuntu 的 Apache 默认是 LANG=C

比如PHP脚本中需要添加如下代码:

         putenv(‘LC_CTYPE=zh_CN.UTF-8’);

         putenv(‘LANG=zh_CN.UTF-8’);