Mudkip Mud Sport

Mudkip's Mud Sport Journal

iOS 应用中,处理和触摸有关的事件是一个必不可少的工作。UIKit 本身提供了丰富的常用 UIControl 控件,如 UIButton、UISwitch 等,使用这些控件时只需用 addTarget:action:forControlEvents: 或在 Storyboard 拖出一个 IBAction 就可以轻易实现事件响应;另外,各种各样的 UIGestureRecognizer 也可以为任何 UI 元素添加手势,实现所需的事件处理。

然而,许多场景下需要 App 干涉的 UI 元素的事件响应过程。例如,某个 UIButton 在设计中 frame 很小,而希望它的响应区域更大;或者,App 需要在某些情况下忽略 UIView 的层级关系决定响应 Tap 的元素。

iOS 采用一种叫 Hit-Testing 的方式决定接收触摸事件的元素,并提供了 hitTest:withEvent:pointInside:withEvent: 方法允许 View 决定自身是否接收事件,或具体接收事件的 subview,两个方法第一个参数均为一个相对于 View 自身 bounds 的 CGPoint

一般来说,UIView 在响应到触摸事件时会进行如下判定,同时这也是 hitTest:withEvent: 默认的实现:

  • 调用自身的 pointInside:withEvent: 方法
  • 若返回为 NO,则拒绝接受该事件,hitTest:withEvent: 返回 nil,Game Over
  • 依次调用自身所有 subview 的 hitTest:withEvent:,调用的顺序从最上层的 subview 开始,直到最底层的 subview 或中途返回了非 nil 的 UIView。
  • 如果在这个过程中遇到了非 nil 的 UIView,则把这个触摸事件交给它,自身的 hitTest:withEvent: 也会成功返回这个 UIView,到此为止。
  • 如果所有 subview 的 hitTest:withEvent: 方法返回均为 nil,则自身处理这个触摸事件,hitTest:withEvent: 方法也会返回 self。

pointInside:withEvent: 默认实现想必正如字面意思,判定触摸的 CGPoint 是否位于自身 bounds 内。另外需要注意的是,userInteractionEnabled 为 NO 的元素不会响应事件,如 UILabel 和 UIImageView 默认为 NO。

回到之前提到的场景,当 UIButton 自身 frame 过小,如何扩大它响应触摸的区域。一个最常见的做法是改变它的 frame,同时使用 imageEdgeInsetscontentEdgeInsets 等方式为它增加 padding,使 UIButton 的 frame 扩大而不增大它其中的内容。

但个人认为这并不是一种非常优雅的方式,设计师同学在设计时,考虑的一般是内容边缘距离父级元素或同级其他元素的距离,对应到用 Auto Layout 开发时,也就是一个相同数值的 NSLayoutConstraint。但若为了改变它的事件处理而改变 frame,配置约束时就会变得复杂和混乱。

而重载这个 UIButton 的 pointInside:withEvent: 方法就可以很好地解决这个问题,由于这个方法的第一个参数是相对于自身 bounds 的 CGPoint,只要判定这个点位于自身 bounds 外围指定距离内的 CGRect 内时返回 YES,就可以在 UIButton 外围响应触摸事件。

横滑滚动多个可以纵向滑动的 UIScrollView(UITableView)也是很多 App 中一个经典的交互设计。而若当这些纵向的 UIScrollView 需要一个公共的 ‘headerView’,并且在它们上滑的时候,希望 UIScrollView 中的内容能遮盖或推走这个 headerView,甚至这个 headerView 还存在可以响应事件的按钮的时候,问题就变得稍微有点微妙。

由于这个 ‘headerView’ 并不希望它属于每个纵向的 UIScrollView,不随着横向滑动而移动,所以它应当独立于横向滑动的 UIScrollView(UICollectionView)之外;由于希望纵向的 UIScrollView 滑动时可以用内容遮盖 headerView 的内容,所以它的层级应该位于横向的 UIScrollView 之下;另外,纵向的 UIScrollView 应该用 contentInset 或透明的 tableHeaderView 让下层的 headerView 露出来。

看到这里,我想很多同学会疑问“为什么不用两个纵向的 UIScrollView 嵌套起来”,不过想象一下在滑动内层的 UIScrollView 如何处理就变得头大了吧。

此时,这个 ‘headerView’ 由于位于下层,无法响应事件,如果其中有 UIButton,也无法被点击。而利用重载横向滑动的 UIScrollView hitTest:withEvent: 方法,在点击区域位于“本应属于 headerView”的位置时返回 nil,就可以很好地解决这个问题,它的 superview 也自然会把这个事件交给下一个 subview,即这个 headerView 来处理,而这个 headerView 亦会把事件交给对应的 UIButton,最终完成按钮的点击。我在处理这个问题时利用了一个透明的 tableHeaderView,在 hitTest:withEvent: 中调用了它的 super 方法,如果拿到的 UIView 正是这个 tableHeaderView,就直接返回 nil,否则返回 super 的结果。

关于这两个问题,我写了一个简单的 Demo,需要的童鞋可以移步 GitHub,有不同的想法欢迎扔 Issue : )

「如果说,有一个和我们所在的世界不同的丰缘地方,神奇宝贝的进化有点不同,也许不存在超进化,也许三千年前没有那场灾难。」——那同样是一个不能破坏的安宁的世界。

「或许存在另一个世界,你阻止了的人是我。」——在他的梦中,被唤醒的超古代神奇宝贝是另一只。

祐树来到未白镇的这一天,神奇宝贝多功能领航员上出现了未曾谋面,但却熟悉的面孔,通信画面上接下来的一言一行,都似记忆深处早已发生的真实。110号道路与小遥的对战,在强烈的既视感之中「再次」完败——唯一不同的只有沼跃鱼倒下前的神态。

在历史的长河中,总有许多命运的分界点上,自然亦或某个生命做出不同的选择,产生了错综复杂,但又相似收束的世界线。在某个时空中,紫菫没有成为室内都市,但那里的游乐场也没有因不能说的理由关闭;在某个时空中,关都四天王有不同于正义使者的过去;在某个时空中有会说话的喵喵和两人组成的三人组。

每一个生命都或多或少有种「命运探知」的能力,相信不只是曼珠、主角和敌对组织首领,每个人都有过Déjà vu的体验。如AG086,一个时空中某个虚构作品,恰是另一个时空的真实。如果能读到其他世界线中自己的记忆——甚至像XY036中一样,和另一个自己见面,一定可以「创造」或者「改变」什么。

2014年接触了很多平行世界题材的作品,Ever17的轮回,Virtue’s Last Reward的选择,命运石之门的跳跃;那些为了重要的东西努力改变世界线的故事,每一作都有很深刻的感动。至ORAS变幻篇章的最大收获,不仅仅是坚持「相信」喜欢的世界的存在,而且对于「哪一个」才是「真实」,也有了答案。

不过呢,在这个世界线变动率附近的范围内,现在这个数值就刚刚好呢: )

圣诞快乐。

参考:AzzyFox

image-1

In Sootopolis City, after the end of the extreme weather event.

Finally, it’s just two of us again.
I’ve got to say…thanks for everything you’ve done for our region, Swam.
You know, I thought we were supposed to have set out from Littleroot together, but…
But you, Swam…
It seems like you just keep getting further and further ahead of me…
I even started to feel like you’d gone somewhere I can’t even reach.
It’s pretty lonely, you know?
Not!
What would you think if I really said something like that?
Hee hee!

My dear May. Do you know what I’m thinking?

I mean, everything I were able to have done, begins with our first battle near the pool of Route 103, just like you said in Lilycove. I were merely a careless boy who knew nothing about Hoenn, but you welcomed me with the most beautiful smiling face on that day, and taught me how to battle, how to creep near Poochyena. You helped me and encouraged me every time.

I never feel I’m ahead of you. When we met at Lilycove, I noticed your Pokédex was nearly completed. You must have made great efforts to achieve this, and that’s the thing I lag behind you a lot. You are my best friend, and you are never alone.

So…When our journey comes to a new chapter, I wish we get back to Littleroot together, and I want we understand everything about each other and our Pokémon and share our journey together.

And…May we be together forever.

一周多前终于完成了3DS上的「Zero Escape: Virtue’s Last Reword」(極限脱出ADV 善人シボウデス)这部作品。40小时的游戏时间过后,虽然很多谜题和真相解开了,但是留下的困惑和谜题更多了,希望续作(如果有的话)能够一一释疑吧。

两年多前玩过DS上的前作「9小时9个人9之门」;VLR本作是美版发售就买来了,但因为等Bug修复补丁等了很久(现在也没出)加上胆小不能在平时玩(?)就拖了一年多,然后最近终于想起来一定要体验这作,差一点就错过了。

故事发生在2028年12月25日,前作的一年后,主角和其余8人被绑架到神秘的设施中,被迫参与密室逃脱以及囚徒困境的AB(Ally or Betray)游戏。9个人在游戏开始被佩戴了计数为3的腕表,当在进行几轮AB游戏后计数达到9时就可以脱出游戏,而计数小于等于0时则会遭到「处罚」。和前作一样,这场脱出游戏只是精心设计的密谋,并不是故事的真相。

以下内容涉及剧透,没有玩过本作品的同学请直接前往任天堂3DS eShop商店购买(或PSN购买Vita版),在完成全部结局后再回来看吧。这里推荐下载版是因为传说3DS版有在某个密室(PEC)存档可能损坏存档的Bug(其实我觉得也可能是Feature),下载版可以随时备份存档避免悲剧发生时的损失。

希望

其实一开始对极限脱出系列有兴趣有制作商Spike Chunsoft的原因,某种程度上「9小时9个人9之门」能找到「神奇宝贝不可思议的迷宫 时/暗之探险队」的影子,剧情中贯穿着隐藏的线索,时间和空间的融合,以及真相结局中包含着的「希望」。

两年前玩999的时候,相当喜欢「四叶草」的设定,四叶草书签的希望·信赖·爱·幸运是大家最终走到真相所必需的东西。而本作也有「希望」的要素,日文标题中的「シボウデス」可写作「死亡」不过也可以写作「志望」吧。

但是游戏的结局并没有看到「希望」,更多的是一种「宿命」的束缚,意识回到过去的Sigma的阻止根号6病毒扩散拯救世界的努力失败,而对45年间的Sigma来说,这一切是过去发生的事实,也因此(?)需要付出45年的努力实施阻止根号6病毒扩散的AB计划。

本作四叶和Alice登场的原因仅仅是为了增幅Sigma和Phi的能力,加上四叶的结局一如既往(?)是最惨痛的多少有些遗憾。但是四叶的人设依旧喜欢就是了,对于主角某些不合年龄(?)的表现的反应也是有趣的地方。

视点

在最终章「Another Time」中玩家的视角变成了K,但K却知道很多他「不该知道的事情」,茜最后表示「你不是K,你不需要遵守我们的规则,你是我们的系统里一个独特的变量」的时候,瞬间想到了打越钢太郎另一部作品「Ever17」中的设定,然后看到Wiki中讲PSV版中完成最终章的成就叫做「A Certain Point Of View」证实了我的猜测。

「我」就是「视点」,四次元生物「Blick Winkel」,在Sigma博士的影像中其实也暗示了更高次元智慧的存在;但是无论如何套用Ever17中Blick Winkel的所作所为,并不能拯救VLR中的世界,比起挽救两个人的生命,改变整个45年的历史对「视点」来说要难得多。

薛定谔的猫

Reality is shaped by what we believe reality should be.

在接触这作很久之前我就是薛定谔的猫的不科学解释(?)的支持者,因此从来回避体检一类的事情。本作引出了薛定谔的猫(如果外部世界是装着猫的盒子,这个密封设施是观察者),Phi线中也应用了一次这个理论(茜这次并没有被Dio所杀),最终章Alice说茜提到了解决悖论的办法就是「薛定谔的猫」,不过如何应用薛定谔的猫来拯救世界就要等到续作了。

Bluebird

Bluebird是Sigma送给Luna的音乐盒,来源于「メーテルリンクの青い鳥 チルチルミチルの冒険旅行」,包含了笼子里的鸟、幸福得而复失的寓意。

2028年12月31日究竟发生了什么,E路线的世界是否可以存在,Santa在做什么,Phi的真实身份等等谜题都需要在下一作中揭示。但由于日本销量不佳的原因,Spike Chunsoft暂停了极限脱出第三作开发。玩家为了表示对系列的支持发起了Operation Bluebird行动,很短的时间内就得到了上万支持;打越钢太郎目前正在进行其他作品的工作,如果顺利的话,之后极限脱出第三作恢复开发并不是不可能。

所以相信VLR续作一定会登场,比起整个AB计划,Operation Bluebird的成功要容易得多呢。

MediaWiki 最初为运行维基百科而设计的开源程序,目前服务于超过一万的 Wiki 站点。神奇宝贝百科自2010年9月起运行于 MediaWiki,3年多来积累了很多心得,虽然内容和访问量迅速增长,但服务器开支依然保持在负担得起的范围。水跃希望在这里记录和分享。

日志标题中「用得起」的假设是运行在一台 1GB RAM 虚拟(云)服务器,日页面访问量在一百万以内。市场上 1GB RAM 的 VPS 价格约 10 ~ 30 美元每月。MediaWiki 并不适合运行在共享虚拟主机,或内存不到 512MB 的服务器中。

另外,MediaWiki 官方网站提供了一些基本的优化方式,非常值得参考。

MediaWiki 很耗资源么

是的。相较其他流行的 PHP 开源应用,MediaWiki 可以认为更耗资源,尤其是 CPU 使用,一篇包含复杂模板嵌套的文章,在无缓存状态下的渲染时间可能长达数秒,甚至数十秒。

经历了十多年的发展,MediaWiki 也是一个架构设计优雅的工程,具有非常强大的功能和可定制性,其性能开销往往是必要的。合理运用多重优化手段方式,可以有效降低 MediaWiki 的性能开销。

使用高性能的 Web 服务器程序

如果在生产环境使用最新版本的服务器程序,往往会引起很大的争议。但事实上,开源程序往往可以保持对较新版本系统环境很好的兼容性,新版本的系统环境往往具有更少的 Bug 和非常大的性能提升。

52Poké 所在的虚拟服务器采取了较为激进的做法,自2011年后一直使用 Arch Linux 操作系统,其滚动升级的特性可以让包管理(pacman)安装的应用都保持在最新版本,同时 Linode 也默认提供了最新版本的 Linux 内核。虽然对基础服务大版本的升级危险较大,但两年多来数十次 pacman -Syu 并未带来很大麻烦,除了在某几个凌晨造成数十分钟的不可用(都是由于较长时间未升级造成跨度较大并且操作失误)。

回到主题,对 MediaWiki 而言,高性能的 Web 服务器程序除了意味着 PHP 5.5,MySQL/MariaDB 5.5+;也包括使用 PHP 5.5 内置的 OPcache 加速,以及使用 Nginx + php-fpm 代替 Apache,Memcached 和/或 Redis 缓存等。

当前支持神奇宝贝百科的服务器程序为 PHP 5.5.7,MariaDB(MySQL 对社区友好的一个变体)5.5.34,Nginx 1.4.4 以及 Memcached 1.4.17。

合理的配置文件

MediaWiki 的默认配置(LocalSettings.php)并不适合生产环境,这里简要介绍一个低能耗的 MediaWiki 系统需要增加的配置项:

  • $wgDisableCounters = true; 关闭内置的统计功能,如需要统计访问量可以使用 Google Analytics 等外部服务。
  • $wgWellFormedXml = false; 以及 $wgHtml5 = true; 打开 HTML5 标签输出并关闭兼容 XML 语法,可以降低页面的体积。
  • $wgShowIPinHeader = false; 关闭未登录用户在右上角显示 IP 地址,以便使用静态缓存。
  • $wgUseGzip = true; 开启 gzip 压缩,降低页面体积,另外也可在 Web 服务器中配置开启 gzip。
  • $wgMiserMode = true; 关闭实时生成特殊页面;建议同时在 cron 里配置定时任务,定期生成特殊页面。
  • $wgJobRunRate = 0.01; 减少执行任务的频率(即每次请求有1%的几率运行任务);MediaWiki 的任务往往需要耗费数秒的时间执行,默认运行频率为1,一旦插入任务就可能导致 PHP 进程全部被占满;这里同样建议在 cron 里配置定时任务运行 MediaWiki 的任务,以免使任务队列积累过多。
  • $wgInvalidateCacheOnLocalSettingsChange = false; 关闭修改 LocalSettings.php 使所有缓存失效的特性;MediaWiki 的缓存完全失效是非常可怕的,几乎可以迅速致服务器僵死。

服务器端缓存

MediaWiki 的稳定运行,opcode 缓存、object 缓存和页面缓存都是必不可少的。

opcode 缓存

opcode 缓存用于加速 PHP 脚本的执行。PHP 5.5 内置了 OPcache(即之前的 ZendOptimizer+),比起 eAccelerator、XCache 和 APC 有更好的性能,只需在 php.ini 中引用 opcache.so 即可使用,同时 OPcache 也可单独安装在 PHP 5.4 或 5.3 环境中。另外需要注意 PHP 5.5.1 及 5.5.2 内置的 OPcache 和 MediaWiki 存在兼容问题,5.5.3 以上版本已解决。

object 缓存

MediaWiki 可以利用 object 缓存存储包括界面语言文字、文件信息、模板生成的中间产物、最终页面渲染 HTML 等数据。MediaWiki 支持使用 APC、XCache、Memcached、Redis、MySQL 数据库方式缓存数据,但配置中默认未开启 object 缓存功能。在这几种缓存方式中,除 MySQL 数据库外都需要在内存中存储,其中前3种存储方式无法持久化。

这里推荐使用是 Memcached 和 MySQL 结合的方式,配置方式如下:

$wgMainCacheType = CACHE_MEMCACHED;   
$wgMemCachedServers = array('127.0.0.1:11211');  
$wgParserCacheType = CACHE_DB;

Memcached 由于将数据存储在内存中,读写速度非常快;存储相同内容的数据占据的内存空间也是几种方式中最小。但 Parser Cache 由于包含了页面完整的 HTML 文本,需要的体积非常大,同时一旦失效则会需要重新解析所有页面,带来巨大的 CPU 开销;在内存有限的条件下更适合存储到较为持久的 MySQL 数据库中。

神奇宝贝百科在2013年初使用 Memcached 曾频繁遇到 Segfault,因此更换到 Redis + MySQL 结合的方式,但 Redis 耗费了更多内存(仅指在 MediaWiki 的使用方式下),也有相对较大的 I/O 使用。2013年12月 Memcached 发布了 1.4.17 更新,解决了不稳定的问题,因此目前又换回使用 Memcached。

页面缓存

作为一个 Wiki,对未登录用户而言页面几乎是静态的,所以页面缓存可以非常有效;MediaWiki 提供了 File cache 功能,另外也可以使用 SquidVarnish 的功能。而对于只有一台服务器的站点而言,这里更推荐使用 Nginx 自带的 FastCGI 缓存功能。

MediaWiki 默认会根据请求头中的 Accept-Language(即用户浏览器语言)、Cookie 等因素区分不同的页面缓存(可见响应头中的 Vary 行)。但对于单一语言、没有文字变种的 Wiki,只要已登录用户不使用页面缓存,未登录用户访问则看到相同的页面内容即可。

以下是神奇宝贝百科的 FastCGI 缓存配置,为便于介绍去掉了中文简繁处理相关的内容。这里缓存有效期为5小时,可以根据实际情况修改。另外,特殊页面不宜开启静态缓存(如最近更改和随机页面)。

http {
    ...
    fastcgi_cache_path  /var/cache/nginx/wiki levels=2:2 keys_zone=wiki:128m inactive=5h max_size=2048m;
    
    server {
        ...
        if ($http_cookie ~* "52poke_wikiUserID") {
            set $do_not_cache 1;
        }
        if ($uri ~ "^/wiki/Special:") {
            set $do_not_cache 1;
        }
        if ($args ~ "Special:") {
            set $do_not_cache 1;
        }
        fastcgi_cache wiki;
        fastcgi_ignore_headers Cache-Control Expires;
        fastcgi_hide_header Vary;
        fastcgi_cache_key $uri$is_args$args;
        fastcgi_cache_bypass $do_not_cache;
        fastcgi_no_cache $do_not_cache;
        fastcgi_cache_valid  200 5h;
        fastcgi_cache_valid  301 0;
        fastcgi_cache_valid  404 20m;
    }
}

使用页面缓存就会遇到缓存更新的问题,好在 MediaWiki 提供了 PURGE 功能,配置后只要相应的页面有更新就会发出 HTTP PURGE 请求。LocalSettings.php 中的配置如下:

$wgUseSquid = true;
$wgSquidServers = array('127.0.0.1’);

这里只配置了 127.0.0.1 因 52Poké 只有一台前端服务器,并且和 MediaWiki 程序是同一台。不过 Nginx 默认并不支持 PURGE 请求,可以编译安装 ngx_cache_purge 扩展实现,具体操作步骤可以参考这里

对于具有文字变种,如中文简繁之分的 Wiki,则情况较为复杂,今后这里会详细讨论 MediaWiki 处理中文简繁的问题,本篇就不详述了。

减少模板和文章内容渲染的开销

MediaWiki 提供了非常强大的模板和语法处理功能,尤其是内置的 ParserFunctions 扩展,但语法解析需要非常大的 CPU 开销。

这里建议尽可能减少不必要的模板使用和嵌套。例如对于一些通用的文字、背景颜色、边框样式,更适合在 MediaWiki:Common.css 或小工具中使用 CSS 实现。如果文章在编辑后需要等待很长的时间才能加载出来,则需要对其中使用的模板进行优化,必要时可以考虑拆分文章。

MediaWiki 解析图片信息也需要较大的资源消耗,对于较为固定的图片、尤其是尺寸较小的图片,推荐使用 CSS Sprites 而非通过 Wiki 上传。

MediaWiki 也会对包含过多模板嵌套、或者使用了过多语法呼叫(例如 switch 和 if 语法)的文章自动添加警示的分类。

选择合适的服务商

基础硬件的性能往往是第一位的,对运行 MediaWiki 而言,CPU、内存、I/O 性能、网络带宽都至关重要。其实早在2006年 52Poké 就搭建了 MediaWiki,但在有上百篇文章之后当时所在的虚拟主机就不堪重负,不得不在后来转型使用自建的程序,直到2010年开始使用 Linode。

就 52Poké 使用 Linode(推介链接) 的经历而言,Linode 是一家不错的 VPS 提供商,但并不算完美。Linode 在2013年进行了多次升级,在流量、网络速度、CPU 性能和性价比方面有较大优势。

VPS 有一个不稳定因素是 CPU 和 I/O 性能会受同一台物理服务器的邻居影响,52Poké 曾遇到过 CPU %Steal 过高导致性能较差的问题,不过联系客服很快迁移了另一台物理服务器解决。Linode 的 I/O 性能比提供了 SSD 的服务商要差,目前 Linode 只在纽瓦克数据中心提供了测试版 SSD 服务,亚洲用户较多使用的东京数据中心则尚未提供。

其他和未来

到这里已经介绍了高性能 MediaWiki 站点的必要条件。不过对于拥有较多图片的 Wiki 来说,可能很容易遇到流量超限或 I/O 消耗较大的问题,推荐如有必要添加一台流量较为廉价的 VPS 存放或缓存图片;另外可以考虑将图片延迟加载,即当浏览者滚动到图片位置时再加载,该功能的 MediaWiki 扩展在完成开发后会开源。

关于 MediaWiki,除了性能之外,还有很多值得讨论的话题。比如扩展,小工具(Gadget),中文的繁简问题,条目命名和内容规范,自动化批量建设内容,版权问题,以及作为一个 Wiki 社群的建设和发展。这些就留在以后继续讨论吧。

对于未来,如何支持移动设备,如何进一步减少服务器端模板的开销是水跃在继续思考的问题。MediaWiki 官方提供了 MobileFrontend 扩展,但根据设备输出不同的内容会带来更多的复杂度和资源消耗,而且可能降低了移动设备的体验;所以我更希望有响应式的方案。对于复杂的表格如果可以服务器只在页面中输出元数据,由 JavaScript 在浏览器中渲染的话,就可以减轻服务器的压力。但修改 Wiki 的 JavaScript 和 CSS 的权限往往不会开放给普通用户,这样做对社区并不利。

2014年的第一篇终于啰嗦完了,这里祝各位新年快乐,也希望 MediaWiki 的同好有所收获吧: )

0%