Android Activity Task 相关

一直以来 Activity 和 Task 的关系就不是很清楚。即使在前公司修改 framework 时,因为分工也很少接触 Activity 相关的东西,而且就遇到的问题而言,Activity 这块也很少碰到 bug,基本算是个完成度和可靠性相当高的模块,定制 framework 也很少碰它,毕竟就操作系统而言这属于任务调度,这里出了问题那 os 基本也就挂了,核心模块之一。

现在工作接触的 app 开发比较多,所以有必要梳理一下,刷文档的同时随手记录一下。可能有遗漏的知识点,以后遇到问题了再补。

基本关系

  • 启动的 Activity 会被压到当前任务栈(Task back stack)里,每一个 Activity 必然属于一个栈,栈里所有的 Activity 构成一个 task。新的 Activity 会默认被压到栈顶。栈嘛,LIFO。
  • 当按下 back 按钮时,顶上的会被弹出。当最后一个 Activity 被弹出后,task 就不复存在了。
  • 当按下 home 按钮时,当前 Activity 被切到 stop 状态,它所在的 task 被切到后台;当用户通过多任务按钮切换回当前 Activity 时,task 被切换回前台,并且栈顶的 Activity 回到 resume 状态。
  • 一个 Actvity (根据 intent 的 flag 或者在 manifest 文件里的配置)可能会被初始化多次到不同的 task 里。

Task 管理

  • 在多 Window 的系统(7.0+),每个 window 有自己的 tasks。
  • 当 manifest 和 startActivity 的 Intent 里同时有对目标 Activity 的配置时,Intent 的配置项生效。
  • manifest 和 Intent 对 task 的配置并不一一对应,各有自己特有的项目。
  • 在 manifeset 文件里关于 task 的可配置项有:

    • taskAffinity:用于协同 Activity。调度时会先考虑把目标 Activity 移动到相同 affinity 的 Activity 所在的 task 里。
    • launchMode:如何加载目标 Activity 到 task 里。
      • standard 默认形式,加载到当前 task,并且该 Activity 可被初始化多次,不管是初始化到当前 task 还是到其它 task。
      • singleTop 当前 task 的栈顶是目标 Activity 时,该 Activity 将收到 onNewIntent,并且不会被创建新实例;当目标 Activity 不在栈顶或者没有被实例过,那就初始化该 Activity 并压到栈顶。该 Activity 可以被实例化多个,并且可能属于不同的 task,也可能属于当前 task(前提是不在当前 task 栈顶)。
      • singleTask 目标 Activity 有且只有一个实例。如果已经被初始化过,则收到 onNewIntent,否则创建一个新的实例并创建一个新的 task。
      • singleInstance 目标 Activity 有且只有一个实例,并且所属 task 也只有目标 Activity 一个 Activity,其它任何由该 Activity 启动的 Activity 都将被压到别的栈里去。
    • allowTaskReparenting:和 taskAffinity 类似,协同 Activity 之用。当为true时会结合其它配置项,如 affinity,来调整目标 Activity 所在的 task。
    • clearTaskOnLaunch
    • alwaysRetainTaskState
    • finishOnTaskLaunch
  • Intent flag 里关于 task 的可配置项有:

    • FLAG_ACTIVITY_NEW_TASK 和 launchMode = singleTask 一毛一样。
    • FLAG_ACTIVITY_CLEAR_TOP 【Intent flag 限定!!】嘛,就是当目标 Activity 实例已经存在在其它 task 里时,把该 task 里目标 Activity 头上的 Activity 全部弹出销毁,然后调用目标 Activity 的 onNewIntent。
    • FLAG_ACTIVITY_SINGLE_TOP 和 launchMode = singleTop 一毛一样。

Depth for Twitter 发布

Depth for Twitter 的 iOS 版终于上架了🎉

开发这个程序真是耗了不少精力,从2014年底起手开始到现在总共耗费两年的时间。虽然是断断续续的两年,期间要打工挣口饭钱,还有不断的翻工重写,但总的来说开发设计加各种放在它上面的时间也有一年多。现在成功上架,也算舒了口气。

因为最初有借这个 app 来锻炼 iOS 开发的目的,所以一如既往,全部代码依然都是手敲的,没有依赖外部的库,算是一点小小的成就。绝大多数图标也是用 Affinity Designer 亲自完成。

虽然这不是我第一个上架的 iOS app,不过可以说是我花时间最多,也是功能上相对比较完善的一个 app 了。相比 Android 版,我添加了 Streaming API 的支持,实现了 Mute 内容的 iCloud 同步。另外虽然 Android 版在性能上我没有做妥协,但是在 iOS 版里,可以说花了更多的时间去研究如何提高滑动的效率。期间有印象的大型翻工就有3次,还有熬夜到两三点利用 Instruments 一帧一帧的去优化的场景,现在想来还是无比感慨。虽然最终达到理想状态几乎时刻60 fps 的修改是受 YYKit 作者(不得不说一下,真是个牛人)的一篇关于性能的博文的启发,完全将绘制工作放到后台去实现的,和 Instruments 里调出来的没太大关系,但确是很值得回味的一段回忆。

其实当初完成了 Android 版后并没有计划去实现一个 iOS 的版本,因为 Tweetbot 实际已经很完善、功能很健全了。但后来看到 Tweetbot 的卡顿和一些一直想要的功能始终没有在其后续版本里实现,相反网页加载这个点不进反退(是的,我承认用 SafariViewController 可以更好的集成系统提供的功能,以及方便后续开发,但是那个独占的打开方式实在让我不能接受),再加上在 iPhone 上我用的最多的 app 就是 Twitter 客户端,于是一直蠢蠢欲动的“何不把最常用的 app 变成自己写的”这个念头终于开始控制不住付诸实践了。

虽然是花了很多精力完成的一个小作品,但是我很清楚就完成度而言和其它成熟的 Twitter 客户端还是有不小的差距,缺失的功能还很多,比如多用户支持、实时消息的推送等。不过话说回来我也并不想拿这个 app 去和那些高完成度的去比较,毕竟做这个 app 的初衷只是实现一些自己对 Twitter 客户端的需求,在这方面会力求完美。可能最值得一提的就是后台加载网页功能和流畅的滑动效果了,这两点是我目前在其它客户端上看不到的。

趁着发布这个 app,赶紧把搁置了一年多的博客更新一下。其实开发期间有好多想法想要记一笔的,然而实在懒的不行,加上 Xcode 一直处于打开的状态,就会觉得赶紧再实现一个功能多好,于是就略过了。懒,总是有很多理由的:)

setjmp longjmp signal实现简单的try-catch

最近升级了Xcode 7 GM,发现GitDiff 插件不工作了。于是自力更生翻源码找bug。虽然最后发现原因是之前用Xcode-beta 时xcode-select 到了beta 版,和GitDiff 并没有什么关系,但看源码时发现一对看起来很牛逼的函数:setjmp/longjmp。好奇心大起,查阅了一下他们究竟为何物。

简单来说,setjmp/longjmp 类似goto,但牛逼的多,因为setjmp/longjmp 可以在函数间“跳来跳去”。

不得不吐槽这对函数名,实在太不直观了。。。

仅仅这么个作用的话,貌似没什么使用的必要,那GitDiff为何这么兴师动众的调用这么底层的函数呢?翻看了一下GitDiff 的commit 记录,才知道原来还有个主要人物--signal

概括GitDiff 的做法,就是通过setjmp保存好上下文,用signal 函数来截获可能出现的SIGPIPE,然后在SIGPIPE出现时通过longjmp返回出错之前的代码,并通过返回值来避开出错代码,继续往下走。
换句话说,就是实现了一个简单的try-catch机制。这也是setjmp/longjmp/signal互相配合的一个“常见”用法。

关于这个SIGPIPE,简单Google 了一下,貌似用popen时还是挺常见的,一般都需要一套同步机制来保证popenpclose

分开来说

  1. setjmp保存当前上下文环境到参数(jmp_buf类型)里,jmp_buf的细节和系统具体实现相关,我也没再追下去。其返回值的规则是:第一次调用时返回0,表示是从setjmp返回的;之后,如果longjmp被调用,那么程序会根据保存的上下文环境(之前保存的jmp_buf类型的变量,也是longjmp的第一个参数),再次来到setjmp这里,并返回一个由longjmp带过来的值(longjmp的第二个参数),程序可以通过setjmp的返回值来改变流程。
  2. 调用signal函数安装SIGPIPE信号的处理函数。通过man知道signal的返回值是“当前的对该信号(这里也即SIGPIPE)的处理函数”,也就是默认的处理函数,保存这个值很有必要,因为需要在最后进行恢复。
  3. 如果SIGPIPE被触发,那么在之前安装的信号处理函数里调用longjmp。其第二个参数就是返回到setjmp时的值,这里只要给一个不是0的值,就可以标记为“出错”。

虽然最后没有找出GitDiff 的bug,但是收获颇丰:)

UIScrollView的contentInset和Pull-to-Refresh

iOS 6开始加入了UIRefreshControl 来帮助实现下拉刷新 效果,用现成的控件自然是方便,但不明白其实现原理心里还是有些惴惴不安。于是小探索了一下,实现一个基本的下拉效果。

原理很简单,是添加一个Subview 到目标UIScrollViewUITableView)里去,并且设置其frame 使其在显示区域外面(y 等于负的高度)即可。这样当UIScrollView 下滑超出其边界时,会显示出我们添加进去的Subview。
然后在UIScrollViewDelegate 相应的代理函数里,检测到下拉距离超过定义的高度时就可以触发刷新了。

More...

关于Swift 的一点认识:值类型和引用类型,Any 和AnyObject

Swift 1.2对值类型的处理做了很多优化,使其执行效率得到很大提高。国外很多评测文章也指出新的Swift 编译器编译出的代码明显比之前版本编译出的代码快。受其影响,决定将自己项目里的数据结构全部换成struct。经过这一番重构,感觉对Swift 这门语言有了点新的认识。

之前没用过有现代语法的其它语言,像Ruby、Scala 这类。于是看到Swift 那诸多眼花缭乱的“新”特性很是兴奋,一直坚持练习。但对其的理解始终停留在这种语言的表象层面或者说语法层面上,比如强大的enum,很流畅的closure 表达方式、函数式/范型思想的大量运用等等。只是觉得它比起其它古老的语言(Java、Objective-C)等要好用,却没有进行深入了解。

More...

Study of iBeacon

因工作需要研究了一下iBeacon,简单总结一下。

技术概要

Apple 的iBeacon 技术规范里规定iBeacon 的信号广播频率为100ms 一次,即1秒10次。而Apple 提供的SDK里对iBeacon 监听回调函数由系统每隔一秒通知一次。这两个频率相关的内容是无法更改的。

More...

Implement image zooming with UIScrollView

想要在iOS上实现一个简单的手指缩放图片的功能,查了一下用UIScrollView 是一个比较方便的途径,因为其原生支持pinch-zoom的功能,这点真是比Android 好太多。

##实现方式

经过一些搜索和实验,明确了UIScrollView 要实现pinch-zoom,只需做到下面两点:

  • 实现UIScrollView.viewForZoomingInScrollView() 函数。

  • 设置好maximumZoomScale 和 minimumZoomScroll 以及zoomScale 属性的值。

More...

Xcode Plug-in: NavigationBarShrinker

最近完成了一个Xcode插件,用来控制编辑器上方导航栏的显示。开发中学到了不少东西,小记一笔。

契机归功于Xcode 6 Beta3,把导航栏改宽了不少,导致垂直可视区域少了一行代码,Yosemite把标题栏缩短的优势也荡然无存。

More...

清除最近打开文件列表的Alfred 2 Workflow

之前装了一个markdown的软件,发现不顺手就删掉了,然后markdown文件就被Xcode关联了(不知是之前就这样还是默认如此)。
于是一不小心Xcode的最近打开列表里多了一些无关紧要的md文件,然后发现不能单独删除,只能一次性全部清空,着实让我不爽。

More...

Depth 1.0 发布

今天发布了做了好久的Depth for Android,一个Twitter客户端,也是我第一个比较正式的应用。
而且刚一发布就发现了一个bug:发推成功后会导致app崩溃。闹了一个大笑话。还被指出了一个英语语法的问题。~_~;

More...