Lambda in Android

Android 如何实现支持 lambda 表达式

lambda 表达式是 java 8 新引入的语言特性,使用了通过 java 7 新引入的字节码指令 invokedynamic 来实现的(参考 Goetz-jvmls-lambda.pdf)。但在 dalvik 中并没有相应的指令,所以直接将 java 8 的字节码翻译为 dalvik 字节码目前是是不可行的。不过从 java lambda 的实现上来讲,实际上就是内部匿名类的语法糖。

既然是语法糖,那就是一个代码转换的事,把这个过程抽离出来另外实现,就可以在低版本的 jdk 中实现对 lambda 的支持。retrolambda,就是在字节码层面实现这个转换。retrolambda 的具体实现是基于 java 8 对 lambda 的底层实现来做的。在编译时,java 主要为当前类生成一个方法,方法体(method body)就是 lambda body,这个方法称为 desugar 方法。运行时,第一次执行到这条 lambda 语句的时候,invokedynamic 调用引导方法(BSM),引导方法生成一个实现了具体函数式接口(Functional Interface,只有一个抽象方法的接口)的 VM 匿名类,这个类主要用于捕获 lambda 所需要的变量。第二步,把这个对象的构造函数和 invokdynamic 绑定起来,最后调用这个构造函数返回这个匿名类的实例,也就是所谓的 lambda object(以后再执行这条 invokedynamic 指令就是直接调用构造函数返回实例了)。调用的时候,再把接口方法需要的参数和捕获的变量传递给 desugar 方法来完成 lambda 的应用(可参考理解 invokedynamic)。

retrolambda 的做法是,源文件先用 java 8 编译,lambda body 转换为当前类的 desugar 方法编译器已经处理好了。接着解析编译后的 class 文件,遇到一条 invokedynamic 指令,就模仿它调用它的引导方法(LambdaReifier.reifyLambdaClass),把引导方法生成的匿名类作为当前类的匿名类保存下来,接下来还会对这些类再做一些变换,包括用单例优化无状态的 lambda 对象,将构造函数替换为工厂方法(BackportLambdaClass#visitEnd)。最后把 invokedynamic 替换为对该匿名类的实例化语句,就是这样把 invokedynamic 替换为等价的兼容代码。不过, retrolambda 的实现依赖于 java 对 lambda 的具体实现,后续的 java 版本不用匿名类了,那么 retrolambda 也就不能用了。

在 Android Studio 3.0 之前,要在基于 java 的 Android 开发中使用 lambda 表达一般都是用 retrolambda 来转换为 dalvik 能处理的字节码来实现的(就不提夭折的 Jack 了)。 不过 Android Studio 3.0 后,IDE 已经支持实现这个转换了,简称 desugar。具体如何开启可参看官方文档:Use Java 8 language features。IDE 的 desugar 过程比 retrolamda 的主要区别就是时机不同,原理上大致是一样的,IDE 的实现可见 LambdaDesugaring#visitInvokeDynamicInsn。 retrolambda 只能对当前项目进行转换,IDE 是在转换为 dex 之前做的转换,也就是说 IDE 还支持第三方用 java 8 编译的库。

理解 invokedynamic

inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。所谓应用级的代码其实是一个方法,在这里这个方法被称为引导方法(Bootstrap Method),简称 BSM。BSM 返回一个 CallSite(调用点) 对象,这个对象就和 inDy 链接在一起了。以后再执行这条 inDy 指令都不会创建新的 CallSite 对象。CallSite 就是一个 MethodHandle(方法句柄)的 holder。方法句柄指向一个调用点真正执行的方法。

理解 MethodHandle(方法句柄)的一种方式就是将其视为以安全、现代的方式来实现反射的核心功能。

一个 java 方法的实体有四个构成:

  1. 方法名
  2. 签名--参数列表和返回值
  3. 定义方法的类
  4. 方法体(代码)

Android Architecture Component -- Lifecycle 浅析

Lifecycle

LifecycleAndroid Architecture Components 的一个组件,用于将系统组件(Activity、Fragment等等)的生命周期分离到 Lifecycle 类,Lifecycle 允许其他类作为观察者,观察组件生命周期的变化。Lifecycle 用起来很简单,首先声明一个 LifecycleObserver 对象,用 @OnLifecycleEvent 注解声明生命周期事件回调的方法:

public class LifecycleObserverDemo implements LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    void onAny(LifecycleOwner owner, Lifecycle.Event event) {
        System.out.println("onAny:" + event.name());
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void onCreate() {
        System.out.println("onCreate");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void onDestroy() {
        System.out.println("onDestroy");
    }
}

解决 ViewPager 嵌套导致的 Fragment 菜单错乱

以下图嵌套的 ViewPager 为例,它是一个两层嵌套的 ViewPager,也就是说 ViewPager 里面是 Fragment ,每个 Fragment 里面又是一个 ViewPager。在下面的例子中,每个 Fragment 都有一个相同名字的菜单项,可以看到不在当前页显示的 Fragment 它的菜单项也显示出来了。用户滑动到 B ,ViewPager 能正确处理第一层的菜单,显示 B 的时候同时预加载 A、C 两个 Fragment,而菜单里只显示 B 的菜单项。到第二层就有问题了, BA 是第二层当前的 Fragment,它的菜单项也能显示出来,这没问题。但却多出来 AA 和 CA,这是因为 ViewPager 预加载了 A,A 里面的 ViewPager 把 AA 当成是当前 Fragment,把它的菜单项也显示出来了。CA 也是同样道理。

决定是否显示菜单的代码是由 PagerAdapter#setPrimaryItem 实现的,属于主项(primary item)的 fragment 才会显示菜单项。以 FragmentPagerState 为例,具体代码如下:

Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
    if (mCurrentPrimaryItem != null) {
        mCurrentPrimaryItem.setMenuVisibility(false);
        mCurrentPrimaryItem.setUserVisibleHint(false);
    }
    if (fragment != null) {
        fragment.setMenuVisibility(true);
        fragment.setUserVisibleHint(true);
    }
    mCurrentPrimaryItem = fragment;
}

分享我的 Android Studio Emacs 风格快捷键

QWERTY 键盘区域,来自 wikimedia

Emacs 风格的快捷键通过前缀键来扩展更多打字区的快捷键,尽量把快捷键控制在打字键区,显著减少编码过程手腕的移动,是个经得起考验的快捷键方案(非 emacs 用户可能深痛恶觉)。

这套快捷键在 Emacs keymaps 的基础上进行自定义,首先减少对功能键区的使用,一来容易与系统快捷键冲突,二来手指移动的幅度过大难定位不容形成肌肉记忆,所以只保留编译运行相关的快捷键。至于编辑键区则更次,手腕必须得移动,眼睛也得跟着辅助定位,只保留少部分不常用的默认快捷键。数字键区最糟,手腕移动幅度最大,再说我的 87 键盘都没有小键盘-_-,直接弃用。至于鼠标,那更是万恶之源,整个手臂都得移动,还要眼睛配合才能用鼠标完成一次操作,写代码的过程大多是用鼠标辅助点击几次,然后又回到打字区继续敲,这样来回一次切换成本太高。何况程序员经常用鼠标点点点?多没 B 格啊。虽说如此,不过想要完全不用鼠标还是不太容易,只能说一个命令通过鼠标打开层层菜单来执行超过一次,第二次就应该用 Find Action 来执行,如果一天超过三次那就应该给它设个快捷键并记住。

Android Studio 相比 Eclipse 内置的 Emacs keymaps 强大了许多,不过 Eclipse 有 Emacs+, Android Studio 却没有这方面的插件。所有 Android Studio 相比 Emacs 多了一些不足,比如:

Android 项目打包到 JCenter 的坑

搜索下如何发布 Android 项目的信息,大部分都会找到这篇文章 Publishing Gradle Android Library to jCenter Repository,中文的指引可以看使用Gradle发布项目到JCenter仓库。不过,如果按照这些文章提供的 build.gradle,可能还会遇到一些坑。

调用 getBootClassPath() 出错

具体的错误信息是

Cannot call getBootClasspath() before setTargetInfo() is called.

这个是 gradle 的 android plugin 1.1.0 版本的 bug,见 Issue 152811 - android - Android Gradle Plugin 1.1.0 breaks Javadoc tasks。将插件更新到 1.1.1 以上版本就可以了。

classpath 'com.android.tools.build:gradle:1.1.2'

GBK 编码问题

手伤回顾

这是写于上个月的日记:

八月五号晚和朋友一起踩单车,从东里到隆都再到莲下,最后出国道324再回家,本来没想踩这么远,在东里踩了一段后,发现再继续只有这条路线而已,往回踩又没意思,于是只能继续硬着头皮踩下去。

大概是因为侥幸心理作遂,朋友有带手电,我便不想带了。

隆都那段路路况并不好,偶尔一段路面凹陷,避开了几处,还是有一处避不开,因为没有手电看不清路面,高速(20多)压过去,结果后胎被蛇咬了。泄风不快,不过跑了一段后还是停下来打算换胎。因为随身带的内胎是18/23,而后胎是25的,最后还是决定补胎为好,折腾了一段时间才补好继续上路。

爆胎是其一,真正悲剧是发生在隆都到莲下那段路,一开始路况很糟糕且不说。后面的路面是极好的,因为刚修好,还没有通车。我便开始放松了起来,不自觉速度也快了,不过最多就是二十多,因为一直跟着山地后面。变换下姿势,因为觉得架子比较小,身体伸展不开,便把手放到最远端,手掌朝下按住手变顶部,没留神自己居然这个压到手刹上了,结果车轮抱死,只记得前轮好像打横了,其他还没反应过来整个人就飞出去了,可能是左手先着地,伤得最严重,接着是右手侧身落地,肘关节肿起一块,肩膀在地上磨了一小段,起了一块红点,肌肉痛,幸好衣服没破。右脚膝盖也磨掉一小块皮。

起来后发现左手腕动不了,手腕还得放在大腿上支撑,不然痛得不行,剩下的路程只能一只手骑了,十多公里还是坚持下来了。快到的时候发现还被补了一刀,本来车子无大碍,就是手变被磨伤了,结果回来的路上把 cateye 码表弄丢了,估计是在等红灯的时候,一只手上下车很吃力,试了好几次,估计在那里蹭掉了。本来那个红灯可以不等的…看来人生的戏剧性就是体现在这样接二连三的悲剧中。

浅谈 Recycle 机制

这里的 Recycle 机制并不是指 Java 虚拟机中的垃圾回收机制,而是 Android 框架里十分常用的一种设计模式。基本思想很简单,当一个对象不再使用时把它储藏起来,不让虚拟机回收,需要的时候再从仓库里拿出来重新使用,这就避免了对象被回收后再重分配的过程。对于在应用的生命周期内(或者在循环中)需要频繁创建的对象来说这个机制特别实用,可以显著减少对象创建的次数,从而减少 GC 的运行时间。运用得当便可改善应用的性能。唯一的不足只是需要手动为废弃对象调用 recycle 方法。

如何实现?首先,我们需要一个仓库用于存放暂时不用的对象;需要新对象的时候我们不能使用 new 来分配一个新对象,所以还需要一个方法 obtain 来从仓库里获取对象;最后,便是 recycle 方法了,用于回收不再使用的对象。一个简单的实现如下所示,技术细节在注释里解释:

/**
 * Created by Tiou on 2014/7/15.
 * 一个实现 Recycle 机制的对象
 */
public class Data {
    /**
     * 对象池,就是上文所提到的对象仓库,用于暂时存放不用的对象。
     * 用链表来实现对象池结构,直观,高效,易用。
     * sPool 便是指向链表头部的引用
     */
    private static Data sPool;
    /**
     * 指向链表中的下一个元素,当 next 为 null 时表示已到达链表末端
     */
    private Data next;

    /**
     * 隐藏构造函数,避免对象被 new 关键字创建
     */
    private Data(){}

    /**
     * 从池里获取一个新对象,没有的话则返回一个新的实例
     * @return 可用的新对象
     */
    public static Data obtain(){
        if(sPool!=null){ // 池中有可用的对象
            // 对于对象池来说顺序并没有关系
            // 这里取链表的第一个对象,主要是因为方便
            Data data = sPool;
            sPool = sPool.next;
            data.next = null;
            return data;
        }
        return new Data();
    }

    /**
     * 将当前对象回收,一旦对象被回收,便不能再使用,代码中也不应存有任何到该对象的引用
     */
    public void recycle(){
        clear(); //清理对象
        // 把当前对象作为首元素按入链表中
        next = sPool;
        sPool = this;
    }

    /**
     * 重置对象到刚初始化时的状态
     */
    private void clear(){

    }
}

本地生成二维码的 Chrome 扩展

最近谷歌被全面封杀,用了多年的 QR-Code Tag Extension 基本处于不可用状态,特色版 Firefox 自带有本地生成二维码(QRCode)功能,于是便想找一下 Chrome 有没有在本地生成二维码的扩展,结果没有发现,反而找到了一个 jquery.qrcode.js(主要实现绘制的工作,生成二维码的功能是 QR Code Generator 实现的),一个可在本地生成二维码的 jquery 插件,很易用。想必用它做一个 Chrome 扩展是个很容易的事情,毕竟之前也写过 Chrome 扩展,虽说是两三年前的事。

本来以为一两个小时可以搞定,结果从完善功能到打包发布可能得花掉一天时间,API、流程基本忘光了,都得跟着文档慢慢做。jquery.qrcode.js 中文乱码,让它支持 UTF-8 编码费了不少时间,contextMenus + Programmatic injection 也比较麻烦,还尝试下 i18n,样式用的是 Pure.

可以在 Chrome WebStore 下载 Offline QRCode Generator

  1. 提供了最基本的扩展工具栏按钮,点击可直接为当前标签的 URL 生成二维码,也输入自定义文本来生成。
  2. 支持页面右键菜单,可为当前页面网址,所选文本或所选超链接生成二维码。
  3. 使用 Event Pages 技术,只有当需要时(点击右键菜单的时候)才会加载后台进程。
  4. Lazy Load 技术注入脚本,只有点击右键菜单生成二维码的时候,才会为当前页面注入脚本,不会对正常浏览网页产生影响。

新的主题

当晚上把代码提交到服务器后,我确实后悔了,花了不少时间弄了这个新主题,深深明白这样的行为是在舍本逐末。

写独立博客步入第五年,以为很长,又不过是转瞬。虽然到现在还是没什么内容,但摆弄博客所花的时间其实不少,只不过大部分都用于折腾博客系统本身,从 WP 到静态博客,从 php 到 ruby,php 始终不是一门让人看一眼就能喜欢的上的语言,我想,说 php 是专为 Web 开发设计的语言应该没有问题。不可否认,我对 php 对认识还很浅薄,拿来就用,没有系统的学习过,写过 WP 插件,还有一些 CLI 脚本仅此而已。ruby 则不同,一眼便让人着迷,虽然 ruby 因 Web 而火了起来,不过吸引我的却是它的语法和社区,我接触 ruby 两年来还未用过 RoR,只是用来写各种日常脚本和折腾 ruhoh,各种元编程黑魔法目眩而神迷。现在 ruby 已经代替 python 成为我的日常脚本语言,不过作为日常脚本语言来说 ruby 各种库看起来似乎远没有 python 那么齐全。

对于编程语言了解不深,太多吐槽不适合我,还是不要说太多免得贻笑大方。说回这个新主题,如今改这个主题另一方面又算是对得起自己了,居然可以耐心地先把前端方面的知识系统地了解一下。看了一遍 JavaScript: The Good PartsJavaScript 秘密花园 也是份不错的指引。相信如今写出来 JS 代码会更加「纯正」,现在页面上的不少效果都是自己写 JS 实现的。另外还断断续续翻了不少 SASS And Compass in Action,学了 Sass 和 Compass 才知道原来自己一直都是拿着木棍跟着那些身披战甲手持长枪的骑士后面乱舞一通。阮一峰写过两篇文章介绍 Compass 和 Sass,Compass用法指南SASS用法指南,也可以参考我的 SASS 笔记

当然用上最好的装备,不见得就可以打胜仗。实际上这个主题越做越不讨我喜欢,我一心只想要两栏的响应式布局,结果往边栏塞入了太多东西,又无法舍弃花俏无用的功能(做得那么辛苦起码得放上几个月再说吧),完全无视自己一直奉承的简洁至上原则啊(话说以前要做复杂其实也做不出来),特别把自己的头像弄得那么显眼,看起来很不协调。在大屏上把字号调大了一些,既减少一行的字数,也能避免留白的浪费,确实能提升阅读体验。这次的笔记主题也做得还不错,想要的东西都能做出来,就是还缺点美化。

近期評論

友情链接