写给Android开发者看的『微信小程序和Android开发的对比』

1. 前言

1.1 微信小程序

微信小程序近期可谓是动作频出,仅最近新增的能力就有:
* 允许个人开发者申请注册小程序、
* 公众号可以打开小程序、
* 可以在 App 分享、
* 支持识别图片二维码打开小程序、
* 可以支持第三方平台配置、
* 新增数据分析接口、
* 代码包大小从1M扩大到2M、
* 推出小程序码
* 公众号关联小程序新规则、
* 公众平台新增快速创建小程序、
* ……

种种迹象表明,微信对小程序的期望值是很大,所以在它推出的几个月效果没到达预期的情况下,之前的很多『克制』也就逐渐变成『放肆』了 —— 不过不管小程序以后的发展到底怎样,对我们开发者来发,多了解一些总是没有坏处的。

1.2 为什么要写这篇文章

他山之石,可以攻玉。
对于是技术人来说,多了解一些不同的技术、不同的开发模式、不同的架构思想,提高技术『广度』,对于自己的成长是十分必要的。
所以,本文就是从一个 Android 开发者的角度,从项目工程方便切入,来分析一下『微信小程序』跟『Android App』开发上的一些异同。
『微信小程序』开发是一个相对较新的技术,希望通过本文,能让你对它多一些了解。

1.3 本文的目标读者

因为内容是从Android开发的角度来谈的,所以我假设你已经对 Android 开发比较熟悉了。并且对微信小程序的开发也比较感兴趣,如果要是再能有些 javascript、css 的基础的话那就更好了!

2. 开发语言的对比

2.1 Android 的开发语言

Android 开发我们已经比较熟悉——
* 主要开发语言是 Java(当然还有 Kotlin/Scala/Groovy,暂时不在本文讨论的范围内);
* 另外使用 xml 文件来描述界面;
* 使用 AndroidManifest.xml + gradle 文件来配置项目;

2.2 小程序的开发语言

作为对比,进行微信小程序开发所用的语言是这些——
* 主要开发语言是 javascript
* 使用 wxml + wxss 文件来描述界面;
* 使用 app.json + app.wxss 文件来配置项目;

2.3 wxml、wxss 是什么?

wxml(WeiXin Markup Language) 基本约等于是 xml。微信之所以没有直接使用 xml ,可能是为了以后扩展方便一些(野心很大)。
同理,wxss(WeiXin Style Sheets) 基本约等于是 css。也是微信扩展了一些功能,比如统一的尺寸单位 rpx

2.3.1 小程序为什么要使用 wxml + wxss 来描述页面?

对于 Android 来说,对于页面的描述基本上在 xml 中定义的,比如:

<framelayout android:id="@+id/layout"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">
  <textview android:id="@+id/textViewHello"
    android:textSize="20sp"
    android:textColor="#f00"
    android:layout_width="200dp"
    android:layout_height="wrap_content"
    android:text="Hello World!"></textview>
</framelayout>

这是一个简单的典型的示例,这个文件就是描述了两部分内容:

  1. 页面结构: 一个 id 为 layout 的 FrameLayout, 它的内部包含一个 id 为 textViewHello 的 TextView 。
  2. 页面样式: FrameLayout 和 TextView 各自的属性:width、height,TextView 还有字号、颜色、文字内容等。
    而微信小程序所使用的 wxml + wxss 方式:
    some.wxml:
<view class="userinfo">
  <image class="userinfo-avatar"></image>
  <text class="userinfo-nickname">张三</text>
</view>

some.wxss:

.userinfo {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.userinfo-nickname {
  width: 128rpx;
  color: #aaa;
}

很明显可以看出:wxml 是负责了页面结构的展示;而 wxss 则负责了对页面样式的定义。
这种把结构和样式分离的做法,其实是延续了网页开发中的习惯(html + css)。
这样做的好处起码有两个:
* 一是使代码的职责划分更加明确,结构清晰。
* 二是可以避免许多重复的代码。(你在写 Android 的 xml 文件时,有没有经常碰到有许多重复的属性,需要抽离成一个 style 出去的?这就是基本是样式表的思想了)

3. 工程结构对比

3.1 小程序的工程结构

小程序的工程结构

——看起来还是挺简单的结构:
* pages
* index
* index.js
* index.wxml
* logs
* utils 文件夹
* 三个以app命名的文件: app.js 、 app.json 、 app.wxss
我们来梳理下文件功能:

3.2 app 相关: app.js 、 app.json 、 app.wxss

这三个文件用以描述小程序 app 相关的内容,他们的命名是固定这样的,位置也固定是在根目录下。

3.2.1 app.js

app.js 基本相当于 Android 中的 Application 类,文件中主要是有一个 App()函数,来进行小程序的初始化操作。
* 它提供了一些关于整个小程序生命周期的回调函数: onLaunch、onShow、onHide、onError 等。 比如如果开发者有一些需要在小程序启动时执行的操作,就可以在 onLaunch 函数中执行。

【对比 Android】我们在 Application 的 onCreate 函数中的初始化操作。
* 它提供了一个 globalData,用以存储整个小程序使用期间的公共数据。
【对比 Android】有时候我们也会在自定义的 Application 放一些数据,供不同的页面来使用。
* 小程序提供了一个全局的 getApp() 方法用以获取小程序的 App 实例。
【对比 Android 】想一下 getApplication() 方法~ 😀

3.2.2 app.json

app.json 的作用跟 Android 中的 AndroidMainifest.xml 文件很相似 —— 都是静态化的配置文件。
* 声明、设置各个页面的路径:页面必须要先在这里声明之后才能使用。
* 配置页面的样式(导航栏颜色、文字字体等)
* 配置底部的Tab栏
* 设置通用的TimeOut时间、是否是debug模式等

【对比 Android 】Activity 也是同样需要在 AndroidMainifest.xml 文件中进行声明,另外 App 的主题也可以在这里设置。

3.2.3 app.wxss

app.wxss 定义全局的样式 —— 其定义的样式会作用于每个页面。比如在 app.wxss 中加入:

text {
  padding:5px;
}

就可以给所有的 text 控件添加 5px 的 padding 。
当然,页面本身的 xxPage.wxss 可以定义局部样式来覆盖全局样式。

【对比 Android 】Android 中暂时没有类似 app.wxss 的全局设定,我们需要在每个 layout.xml 中定义每个控件的属性,即使可以用 style 文件来抽离一些统一的样式,但还是需要在每个 View 的属性上去设置使用这个 style —— 从这方面看,使用 wxss 更有优势一些。

3.3 utils 文件夹

根目录下的 utils 文件夹中有一个 util.js 文件,这个故名思意,是类似于 Java 中的一些工具类的存在。
utils 文件夹其实是一个非必须的结构,而它之所以出现在官方的 HelloWorld 工程中,是作为一个代表,表明了开发者在这里是可以自定义新的文件夹和结构的。微信小程序作为一个使用 js 来开发的平台,是可以使用许多第三方的 js 库的,对于这些第三方库,以及其他的图片资源等,都可以放到自定义的文件夹中。

3.4 pages 文件夹

pages 文件夹下包含两个子目录:index 和 logs ,两个目录的结构都是基本一样的,都是包含四个相同主名称的文件: xx.js、xx.wxml、xx.json、xx.wxss 这几个文件。
这样的一个典型结构表明它是一个小程序的页面,四个文件的作用分别是:
* xx.js : 页面的主要逻辑 【相当于 Android 的 xx.Activity 】
* wxml :页面的结构 【相当于 Android 的 布局文件 activity_xx.xml 的结构部分】
* wxss :页面的样式 【相当于 Android 的 布局文件 activity_xx.xml 的样式部分】
* json :页面的配置,作用跟上文讲的 app.json 作用差不多,但只能配置 window 相关的属性,会覆盖 app.json 中的同名配置。
在 Android 中 ,xx.Activity 和布局文件 activity_xx.xml是在不在同一个目录下的,需要使用 setContentView 来把他们相关联。
但是在微信小程序中,这个工作不需要手动去做,微信强制要求需要一页面相关的四个文件需要命名一致,并且要放在同一个文件夹下。

4. 视图更新方式对比

在视图的动态显示上,微信小程序使用了数据绑定(data-binding)的方式。
如果你之前使用过 AngularJS 或者 Vue.js 等这些流行的 js 框架,那么你肯定对数据绑定并不陌生。它是一种把一个控件的属性绑定到某个数据对象(view-model)的属性的方法,这样在改变数据对象属性的时候,所对应的控件属性也就会相应变化 —— 在开发中,这种方式会使得对 View 层的显示控制变得十分简单、自然。
基于此,软件工程的流行架构方式也在之前的 MVCMVP 之外,又多了一个 —— MVVM(Model-View-ViewModel)
数据绑定这种方式现在是如此的流行,以致于 Android 官方都出了一个 [Data Binding Library] (https://developer.android.com/topic/libraries/data-binding/index.html) 来支持数据绑定,但是由于成熟度等原因,目前还并没有成为主流,Android 中的主流视图显示方式,还是通过开发者手动给每个控件 set 数据。
—— 单从这一点上看,微信小程序的开发模式是比原生 Andorid 要『先进』一些的~ ?

5. 页面的生命周期对比

小程序虽然是和前端 H5 页面一样是用 js 来开发,但是由于它最终运行的平台不再是浏览器,而是和 App 的表现几无二致,所以页面的生命周期也是和 App 差不多的。

一个小程序页面的典型生命周期如下:

  • onLoad :页面加载
  • onReady :初次渲染完成
  • onShow :页面显示
  • onHide :页面隐藏
  • onUnload :页面卸载

对比一下 Android 的 Activity 生命周期 :

  • onCreate :创建
  • onStart :开始(到前台)
  • onResume :显示
  • onPause :暂停(不再在前台)
  • onStop :隐藏
  • onDestory :销毁

微信小程序的页面生命周期稍微简单一些,但主要的思想跟 Activity 生命周期基本是一致的。

6. Debug对比

小程序的官方 IDE 是微信自己出品微信Web开发者工具,它内置了一个小程序的运行环境,本质上是基于 Chrome 内核的一个浏览器框架,算是一个模拟器了。
——它虽然跟 Android 的各种高大上的模拟器相比起来略显简陋,但是基本该有的功能也基本都有(断点、Log、网络监控等),而且由于是基于浏览器内核的页面 DOM 解析,所以运行的速度也是像浏览器打开网页一样流畅,不会像 Android 模拟器那样对系统资源要求很高。
另外,在绑定了开发者账号之后,也可以用手机进行真机调试来调试小程序,所以也能在上线前用不同的机器来进行充分的兼容性测试。

7. 总结

总体来说,小程序作为一个新的形态,从开发的角度,它可以算作是一个【Native开发】和【H5开发】的结合,它吸收了原生开发和 H5 开发的优点。对于前端开发人员和原生开发人员来说,都可以在微信小程序中找到许多熟悉的东西。再细节的许多点这里就不在赘述了,大家如果有兴趣,可以自己上手去体验一下。

综上,自然也就有两种人特别适合去做小程序的开发——H5的前端开发人员,以及之前的 Android/iOS 原生 App 开发者。

微信小程序的开发总体来说是很简单的。
—— 对于前端开发者来说,了解一下原生 App 的一些相关思想即可,这些工作其实只要读一遍小程序的开发者指南基本就差不多了。
—— 而对于原生开发者来说,只要稍微补一下 js 的相关知识(html/css),也基本就差不多可以上手去做了。如果你之前恰好已经有过一些 js 的使用经验,那就不用多说了,花半个小时看一下小程序的文档,直接上!


关于作者 :  
http: //www.barryzhang.com
https: //github.com/barryhappy
http: //www.jianshu.com/users/e4607fd59d0d


刚开了个公众号,用以分享Android相关的干货,求些关注,【不只Android】:
【不只Android】

Kotlin 跟 findViewById 的类型推导冲突问题

记录一个小坑

1 描述

从 version 26 开始,com.android.support:appcompat-v7 中的 findViewById 方法的返回值从 View 改成了 <t extends View>

对于开发者来说,喜大普奔的好处当然是以后终于可以不用在每个 finViewById 方法前面加个丑陋的类型强转了。 但是福兮祸兮,好事的背后也难免会有一些不如意的地方。

比如,如果你在用 Kotlin 的话,项目中可能会有很多类似这样的代码:

// 代码 1 
val textView = findViewById(R.id.textView) as TextView

这是 Kotlin 的习惯写法,种写法实际上是跟下面这种写法是等价的

// 代码 2
val textView : TextView = findViewById(R.id.textView) as TextView

由于 Kotlin 的类型推导特性,我们可以在声明 textView 变量的时候不必显式说明,系统会自动从后面的赋值语句中推测出它的类型是 TextView 。

但是在 version 26 之后,代码 1 的这种写法就会报错了:

Type inference failed: Not enough information to infer parameter T in
fun findViewById ( id: Int ) : T!
Please specify it explicitly

报错信息

意思是没有足够的信息来推断 findViewById 的返回类型。

2 原因

上述错误的本质是类型推导的冲突。

如上所说,我们对 textView 的定义并没有说明其类型,它的类型是从后面的赋值语句中推导出来的。
而新版本的 findViewById ,其返回类型是<t extends View>,这是一个泛型的声明,具体类型则是根据所赋值的变量类型来确定的。

—— 等号的左右两边互相依赖,互相还都没有指明,可不就冲突报错了么!

3 解决方案:

既然是因为『两个相互依赖的类行推导都没有指明类型』,那解决方案自然就是选其中一个指明类型咯。

3.1

在等号左边声明类型:

// 代码 3
val textView : TextView = findViewById(R.id.textView)

3.2

在等号右边表明类型。
诸如这种带泛型签名的函数也是可以在调用时显式地指明类型的:

// 代码 4
val textView = findViewById<textview>(R.id.textView)

4 总结

  1. 这只是个很简单的小问题,很好解决,但是了解其本质的过程才是更让人享受的过程~

  2. 有意思的是:As 默认支持 Kotlin 跟 findViewById 更新这两件事 —— 都是在这次的 IO 大会上宣布的。而且现在(2017.06.05)用 AS 新建一个项目并开启 Kotlin 支持,然后把 support-v7 包升级到 26,就会发现默认的页面就会报这个错 ?…… 希望 Google 能早日改正~


关于作者 :
http://www.barryzhang.com
https://github.com/barryhappy
http://www.jianshu.com/users/e4607fd59d0d

Hi,我们再来聊一聊Java的单例吧

本文由BarryZhang原创,同时首发于diycode.ccbarryzhang.com简书非商业转载请注明作者和原文链接。

1. 前言

单例(Singleton)应该是开发者们最熟悉的设计模式了,并且好像也是最容易实现的——基本上每个开发者都能够随手写出——但是,真的是这样吗?
作为一个Java开发者,也许你觉得自己对单例模式的了解已经足够多了。我并不想危言耸听说一定还有你不知道的——毕竟我自己的了解也的确有限,但究竟你自己了解的程度到底怎样呢?往下看,我们一起来聊聊看~

2. 什么是单例?

单例对象的类必须保证只有一个实例存在——这是维基百科上对单例的定义,这也可以作为对意图实现单例模式的代码进行检验的标准。

对单例的实现可以分为两大类——懒汉式饿汉式,他们的区别在于:
懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。

从它们的区别也能看出来,日常我们使用的较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用嘛~

3. 懒汉式单例

先来看一下懒汉式单例的实现方式。

3.1 简单版本

看最简单的写法Version 1:

// Version 1
public class Single1 {
    private static Single1 instance;
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

或者再进一步,把构造器改为私有的,这样能够防止被外部的类调用。

// Version 1.1
public class Single1 {
    private static Single1 instance;
    private Single1() {}
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

我仿佛记得当初学校的教科书就是这么教的?—— 每次获取instance之前先进行判断,如果instance为空就new一个出来,否则就直接返回已存在的instance。
这种写法在大多数的时候也是没问题的。问题在于,当多线程工作的时候,如果有多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了

3.2 synchronized版本

那既然可能会因为多线程导致问题,那么加上一个同步锁吧!
修改后的代码如下,相对于Version1.1,只是在方法签名上多加了一个synchronized

// Version 2 
public class Single2 {
    private static Single2 instance;
    private Single2() {}
    public static synchronized Single2 getInstance() {
        if (instance == null) {
            instance = new Single2();
        }
        return instance;
    }
}

OK,加上synchronized关键字之后,getInstance方法就会锁上了。如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这端代码也就避免了Version1中,可能出现因为多线程导致多个实例的情况。
但是,这种写法也有一个问题:给gitInstance方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。

3.3 双重检查(Double-Check)版本

Version2代码相对于Version1d代码的效率问题,其实是为了解决1%几率的问题,而使用了一个100%出现的防护盾。那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。
——有没有这样的方法呢?当然是有的,改进后的代码Vsersion3如下:

// Version 3 
public class Single3 {
    private static Single3 instance;
    private Single3() {}
    public static Single3 getInstance() {
        if (instance == null) {
            synchronized (Single3.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}

这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查 Double-Check』。

  • 第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
  • 第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况。

—— 这段代码看起来已经完美无瑕了。
……
……
……
—— 当然,只是『看起来』,还是有小概率出现问题的。
这弄清楚为什么这里可能出现问题,首先,我们需要弄清楚几个概念:原子操作指令重排

知识点:什么是原子操作?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
比如,简单的赋值是一个原子操作:

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而,声明并赋值就不是一个原子操作:

int n = 6; // 这不是一个原子操作

对于这个语句,至少有两个操作:
①声明一个变量n
②给n赋值为6
——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

知识点:什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
比如,这一段代码:

int a ;   // 语句1 
a = 8 ;   // 语句2
int b = 9 ;     // 语句3
int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。
——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

OK,了解了原子操作指令重排的概念之后,我们再继续看Version3代码的问题。
下面这段话直接从陈皓的文章(深入浅出单实例SINGLETON设计模式)中复制而来:

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作

3.4 终极版本:volatile

对于Version3中可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可,Version4版本:

// Version 4 
public class Single4 {
    private static volatile Single4 instance;
    private Single4() {}
    public static Single4 getInstance() {
        if (instance == null) {
            synchronized (Single4.class) {
                if (instance == null) {
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

——也就彻底防止了Version3中的问题发生。
——好了,现在彻底没什么问题了吧?
……
……
……
好了,别紧张,的确没问题了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的。
……
……
……
不过,非要挑点刺的话还是能挑出来的,就是这个写法有些复杂了,不够优雅、简洁。
(傲娇脸)(  ̄ー ̄)

4. 饿汉式单例

下面再聊了解一下饿汉式的单例。

如上所说,饿汉式单例是指:指全局的单例实例在类装载时构建的实现方式。

由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。

4.1 饿汉式单例的实现方式

饿汉式单例的实现如下:

//饿汉式实现
public class SingleB {
    private static final SingleB INSTANCE = new SingleB();
    private SingleB() {}
    public static SingleB getInstance() {
        return INSTANCE;
    }
}

对于一个饿汉式单例的写法来说,它基本上是完美的了。
所以它的缺点也就只是饿汉式单例本身的缺点所在了——由于INSTANCE的初始化是在类加载时进行的,而类的加载是由ClassLoader来做的,所以开发者本来对于它初始化的时机就很难去准确把握:

  1. 可能由于初始化的太早,造成资源的浪费
  2. 如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。

当然,如果所需的单例占用的资源很少,并且也不依赖于其他数据,那么这种实现方式也是很好的。

知识点:什么时候是类装载时?

前面提到了单例在类装载时被实例化,那究竟什么时候才是『类装载时』呢?

不严格的说,大致有这么几个条件会触发一个类被加载:
1. new一个对象时
2. 使用反射创建它的实例时
3. 子类被加载时,如果父类还没被加载,就先加载父类
4. jvm启动时执行的主类会首先被加载

类在什么时候加载和初始化?

5. 一些其他的实现方式

5.1 Effective Java 1 —— 静态内部类

《Effective Java》一书的第一版中推荐了一个中写法:

// Effective Java 第一版推荐写法
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种写法非常巧妙:

  • 对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
  • 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现

简直是神乎其技。

5.2 Effective Java 2 —— 枚举

你以为到这就算完了?不,并没有,因为厉害的大神又发现了其他的方法。
《Effective Java》的作者在这本书的第二版又推荐了另外一种方法,来直接看代码:

// Effective Java 第二版推荐写法
public enum SingleInstance {
    INSTANCE;
    public void fun1() { 
        // do something
    }
}

// 使用
SingleInstance.INSTANCE.fun1();

看到了么?这是一个枚举类型……连class都不用了,极简。
由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。

作者对这个方法的评价:

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

枚举单例这种方法问世一些,许多分析文章都称它是实现单例的最完美方法——写法超级简单,而且又能解决大部分的问题。
不过我个人认为这种方法虽然很优秀,但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。

6. 总结

OK,看到这里,你还会觉得单例模式是最简单的设计模式了么?再回头看一下你之前代码中的单例实现,觉得是无懈可击的么?
可能我们在实际的开发中,对单例的实现并没有那么严格的要求。比如,我如果能保证所有的getInstance都是在一个线程的话,那其实第一种最简单的教科书方式就够用了。再比如,有时候,我的单例变成了多例也可能对程序没什么太大影响……
但是,如果我们能了解更多其中的细节,那么如果哪天程序出了些问题,我们起码能多一个排查问题的点。早点解决问题,就能早点回家吃饭……:-D

—— 还有,完美的方案是不存在,任何方式都会有一个『度』的问题。比如,你的觉得代码已经无懈可击了,但是因为你用的是JAVA语言,可能ClassLoader有些BUG啊……你的代码谁运行在JVM上的,可能JVM本身有BUG啊……你的代码运行在手机上,可能手机系统有问题啊……你生活在这个宇宙里,可能宇宙本身有些BUG啊……o(╯□╰)o
所以,尽力做到能做到的最好就行了。

—— 感谢你花费了不少时间看到这里,但愿你没有觉得虚度。

7. 一些有用的链接

深入浅出单实例SINGLETON设计模式:http://coolshell.cn/articles/265.html
Java并发编程:volatile关键字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html
为什么volatile不能保证原子性而Atomic可以?: http://www.cnblogs.com/Mainz/p/3556430.html
类在什么时候加载和初始化?http://www.importnew.com/6579.html

8. 关于作者

https://github.com/barryhappy
http://www.barryzhang.com

proguard如何会导致json解析失效?

1. 楔子

json数据的解析是这个星球上99%的Android程序猿都会遇到的问题,而其中的大部分,都会使用Gson,故事就是从这里发生……

2. 一开始

通常json数据是从服务端取的,典型的一段json字符串如下:

  { "name": "老张","age": 18}

一般程序猿们会在本地写一个实体类与之对应,比如数据类Person:

public class Person { 
    private String name;
    private int age;
    //……省略中间get/set
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }
}

然后就可以愉快地使用Gson来进行解析啦,像这样:

Person someone = new Gson().fromJson(jsonString, Person.class);
Log.d("TAG", "someone: " + someone.toString());

一切顺利,程序猿们得到输出:

Person{ name=’老张’,age=18}

然后他们就愉快地下班去玩耍了。

3. 后来,他们开启了proguard

意外就是从这里开始的,开启了proguard之后,debug运行的程序一切如常,但是proguard之后输入的apk包,在安装之后的输出变成了这样:

Person{ name=’null’,age=0}

——解析出错了。
(算了,我编不下去了,从现在开始使用第一人称……)

4. 问题所在

经过反复排查,发现问题就是由proguard引起的。
我们知道,proguard的过程会执行压缩、混淆、去除无用代码等操作。而其中的混淆、去除无用代码的过程,对于使用了反射技术的代码来说,都是有可能受到影响的。
不幸的是:Gson.fromJson()就是基于反射来实现的。

通过反编译apk包,发现我们写的Person类,在没有被保护的情况下,会被proguard成类似这个样子的:
反编译代码1
可以看到,类名、字段名都改成了a/b这样的名字。
这样在使用Gson的时候,它尝试去序列化的字段就会变成了a/b,而不是之前的name/age了。
——那还反射个毛啊!

5. 解决

知道问题了,自然也就很好解决了。
方案是在proguard规则中,把Person类给保护起来

5.1 proguard文件编辑

proguard-rules.pro文件中添加规则(也可能是其他文件,具体见gradle文件中的定义),把数据类保护起来即可。
具体来说,可以直接保护当前类:

-keep class com.xxx.beans.Person {*;}

也可以保护整个包:

-keep class com.xxx.beans.* {*;}

还可以给Person添加一个对Serializable的实现,然后保护所有的Serializable

-keep class * implements java.io.Serializable {*; }

5.2 @Keep注解

另外,项目如果引用了注解支持库,那么给Person类加上@Keep的注解也可以防止它被混淆。

@Keep
public class Person {/*省略*/}

OK,加上保护之后,重新打包。
反编译这时的apk,可以看到,字段Person类没有再被混淆了:
反编译代码2
然后安装、运行,果然一切正常了。

Person{ name=’老张’,age=18}

完美。

6. 一些链接

反编译工具:
压缩代码和资源:

proguard相关问题排查:

关于作者:

如何用Canvas画一个正多边形

本文由BarryZhang原创,同时首发于barryzhang.com简书,非商业转载请注明作者和原文链接。

场景

给定一个指定的正方形的区域,要求在该区域内画一个正N边形(正三角形、正方形、正五边形……)

public static void drawPolygon (RectF rect, Canvas canvas, Paint p, int n) {
    // draw……
}

分析

要用到一些三角函数的知识,于是我画了一幅灵魂画作?:
灵魂画作

分析:

  • 计算出每个顶点的坐标,然后把它们连起来,就是一个正多边形啦~
  • 圆心角a的度数为360/n,弧度计算为2π/n
  • 如果把圆心的坐标为(0,0),那么顶点P1的坐标为[X1=cos(a),Y1=sin(a)]
  • 以此类推,顶点Pn坐标为[Xn=cos(a*n),Yn=sin(a*n)]
  • 圆心的实际坐标是外接矩形的中心:[Ox=(rect.right+rect.left)/2 , Oy=(rect.top+rect.bottom)/2]
  • 所以Pn的实际坐标是[Xn+Ox,Yn+Oy]
  • 把P0-P1…Pn连起来就是我们要的结果了。
  • Java中可以使用Path来保存路径,最后使用canvas.drawPath来绘制出来。

实现

简化的伪代码:

float a = 2π / n ; // 角度
Path path = new Path();
for( int i = 0; i < = n; i++ ){
    float x = R * cos(a * i); 
    float y = R * sin(a * i);
    if (i = 0){
        path.moveTo(x,y); // 移动到第一个顶点   
    }else{
        path.lineTo(x,y); //    
    }
}
drawPath(path);

Java代码最终的完整代码,可以直接拿去用:


public static void drawPolygon (RectF rect, Canvas canvas, Paint paintByLevel, int number) { if(number < 3) { return; } float r = (rect.right - rect.left) / 2; float mX = (rect.right + rect.left) / 2; float my = (rect.top + rect.bottom) / 2; Path path = new Path(); for (int i = 0; i <= number; i++) { // - 0.5 : Turn 90 ° counterclockwise float alpha = Double.valueOf(((2f / number) * i - 0.5) * Math.PI).floatValue(); float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue(); float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue(); if (i == 0) { path.moveTo(nextX, nextY); } else { path.lineTo(nextX, nextY); } } canvas.drawPath(path, paintByLevel); }

DEMO

这个项目里用到了这个函数,可以点进去看以及下载demo。
https://github.com/barryhappy/TContributionsView
DEMO


本文链接:http://www.barryzhang.com/?p=510

一开始,我只是想用它来显示一下 Github 贡献图…

近日撸了一个Android的小控件。
一开始,我只是想用它来显示Github贡献图,所以才给它起名字叫TContributionsView,后来,我发现它似乎还有很多更有意思的玩法……
目前想到的可以发展的应用场景有:github贡献图、像素画、电影院选座?棋盘?……

我已经写了三四种Adapter,使得它使用起来非常简单。
项目地址

欢迎大家Star、Fork、提Issue……以及各种提意见。
目前的TODO:

  • 点击事件
  • 自定义绘制格子的支持
  • ……

放几张图给大家感受一下:

demo1
demo2

更详细的介绍、使用方式等:https://github.com/barryhappy/TContributionsView

彻底搞懂startActivityForResult在FragmentActivity和Fragment中的异同

本文由BarryZhang原创,同时首发于diycode.ccbarryzhang.com简书非商业转载请注明作者和原文链接。

1. 前言

Activity、FragmentActivity、Fragment中都有startActivityForResult()方法,也都有用以接收结果的onActivityResult()方法,那他们有什么区别吗?用法上有什么不同吗?

之所以注意到这个问题,是因为最近一次在Fragment中使用了getActivity().startActivityForResult()去调用图片选择器,结果发现在Fragment的onActivityResult无法接收到返回的结果。

仔细研究了一下原因,发现了一些以前没注意到的问题,于是写出来分享给大家。

2. 表现

假设有一个FragmentActivity中嵌套一个Fragment,它们各自使用startActivityForResult发起数据请求。
经测,目标所返回结果数据,能否被它们各自的onActivityResult方法所接收的情况如下:

iamge

  • Fragment和FragmentActivity都能接收到自己的发起的请求所返回的结果
  • FragmentActivity发起的请求,Fragment完全接收不到结果
  • Fragment发起的请求,虽然在FragmentActivity中能获取到结果,但是requestCode完全对应不上

为什么会有这种表现呢?往下看。

3. 找原因:Show me your code !

仔细看文档的话,发现了一个以前没注意到的点:FragmentActivity相对于它的父类Activity,对startActivityForResult的描述是有些改动的。

FragmentActivity.startActivityForResult的文档是这样的:

修改了标准行为,以使它能够把结果传递到Fragment。
添加了一个限制:requestCode必须< =0xffff

这里的标准行为,自然指的是正常的Activity.startActivityForResult的功能。而新增加的对requestCode的大小限制看起来很蹊跷,估计是有什么猫腻在里面了。

OK,不卖关子,直接看源码!

3.1 Fragment.startActivityForResult

从Fragment的startActivityForResult开始:

    public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
        if (mHost == null) {
            throw new IllegalStateException("Fragment " + this + " not attached to Activity");
        }
        mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode, options);
    }

Fragment.startActivityForResult本身的代码很简单,就是调用了一个mHost.onStartActivityFromFragment的方法。
—— Fragment被添加到一个FragmentActivity中之后,这里的mHost即是当前FragmentActivity的一个内部类FragmentActivity.HostCallbacks,它持有对FragmentActivity的引用,mHost.onStartActivityFromFragment被简单转发到当前FragmentActivity的
startActivityFromFragment()方法。

Fragment.startActivityForResult

FragmentActivitymHost.HostCallbacks.onStartActivityFromFragment

FragmentActivity.startActivityFromFragment

接下来到FragmentActivity.startActivityFromFragment:

3.2 FragmentActivity.startActivityFromFragment

public void startActivityFromFragment(Fragment fragment, Intent intent,
        int requestCode, @Nullable Bundle options) {
    mStartedActivityFromFragment = true;
    try {
        if (requestCode == -1) {
            ActivityCompat.startActivityForResult(this, intent, -1, options);
            return;
        }
        if ((requestCode&0xffff0000) != 0) {
            throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
        }
        int requestIndex = allocateRequestIndex(fragment);
        ActivityCompat.startActivityForResult(
            this, intent, ((requestIndex+1)< &lt;16) + (requestCode&0xffff), options);
    } finally {
        mStartedActivityFromFragment = false;
    }
}

分析一下这段代码:
1,mStartedActivityFromFragment = true首先标记一下请求是来自于Fragment。
2,if(requestCode == 1)的内容不用管,它是来自于startActivity(没有ForResult)的情况。
3,然后的代码添加了对requestCode必须小于0xffff的限制 if((requestCode&0xffff0000) != 0){/*抛异常*/}
我们是从Fragment.startActivityForResult追踪到这里的,所以虽然文档没有明确说,但是从这里可以看出:Fragment.startActivityForResult的requestCode也是必须要<=0xffff的。

然后,下面是关键点了:

ActivityCompat.startActivityForResult(
            this, intent, ((requestIndex+1)<&lt;16) + (requestCode&0xffff), options);

——其中ActivityCompat是一个帮助类,ActivityCompat.startActivityForResult最终还是调用的Activity.startActivityForResult,这个先不表。
这里的关键点就是,通过一个requestCode=>((requestIndex+1)< &lt;16)+(requestCode&0xffff)的映射,Fragment.startActivityForResult最终还是调用了Activity.startActivityForResult。

调用了Activity.startActivityForResult其实是意料之中的事情,只是从requestCode((requestIndex+1)<&lt;16)+(requestCode&0xffff)是做了什么呢?

通过分析,得知requestIndex是请求的序号,值为从0递增的整数值。
又从前面得知,requestCode的本身的值是小于0xffff的,所以((requestIndex+1)<<16)+(requestCode&0xffff)简化一下就是:(requestIndex+1)*65536+requestCode
——所以这个值是必定大于0xffff的。

在看一下FragmentActivity.startActivityForResult的代码:

3.3 FragmentActivity.startActivityForResult

@Override
public void startActivityForResult(Intent intent, int requestCode) {
    // If this was started from a Fragment we've already checked the upper 16 bits were not in
    // use, and then repurposed them for the Fragment's index.
    if (!mStartedActivityFromFragment) {
        if (requestCode != -1 && (requestCode&0xffff0000) != 0) {
            throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
        }
    }
    super.startActivityForResult(intent, requestCode);
}

可以看到,判断了一下如果请求不是来自于Fragment,也就是来自于FragmentActivity自身,就限制requestCode不能大于0xffff。

再加上前文所说的,Fragment.startActivityForResult最终映射的requestCode值必定大于0xffff,所以,现在可以得出了一个初步的结果:
SDK把Fragment和FragmentActivity的的ruquestCode都限制在了0xffff以内,然后对于Fragment所发起的请求,都通过一个映射,把最终的requestCode变成了一个大于0xffff的值。

——到现在,已经可以推测到:在获取的结果的时候,也是会通过跟0xffff这个数值来比较,来区分是要把结果交给FragmentActivity还是Fragment来处理。

来验证一下看看:

3.4 FragmentActivity.onActivityResult

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    mFragments.noteStateNotSaved();
    int requestIndex = requestCode>>16;
    if (requestIndex != 0) {
        requestIndex--;

        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            targetFragment.onActivityResult(requestCode&0xffff, resultCode, data);
        }
        return;
    }

    super.onActivityResult(requestCode, resultCode, data);
}

OK,一目了然,证实了我们上面的推论
在FragmentActivity.onActivityResult中,只有requestCode>0xffff时,这里得到的requestIndex才能满足requestIndex != 0,然后进入下面的逻辑:把requestCode通过反向之前的映射关系,还原成最初Fragment所指定的requestCode,交给Fragment.onActivityResult进行处理。

4. 解释最初的问题

所以,现在也能明白了为什么会有前面说的这几个表现:

  1. Fragment和FragmentActivity都能接收到自己的发起的请求所返回的结果

    那当然,就是这么设计的。

  2. FragmentActivity发起的请求,Fragment完全接收不到结果

    被FragmentActivity拦截了,没有转发到Fragment。

  3. Fragment发起的请求,虽然在FragmentActivity中能获取到结果,但是requestCode完全对应不上

    如果是Fragment发起的请求,那么在FragmentActivity.onActivityResult获取到的requestCode,其实是经过映射之后一个的大于0xffff的值,已经不是最初Fragment发请求时的requestCode了。

5. 思考

为什么要用映射requestCode的方法来区分请求是否来自Fragment呢?绕这么一个弯子,直接使用一个变量来标记不行么?

直接使用一个变量来标记还真不行:
* 因为我们自己最终写业务代码MyFragmentActivity肯定是继承自FragmentActivity的,而MyFragmentActivity.onActivityResult的调用会先于FragmentActivity.onActivityResult。
* 所以无论是Fragment还是MyFragmentActivity所发起的startActivityForResult请求,最终在获取结果的时候是一定是会通过MyFragmentActivity.onActivityResult的。
* 如果在这里使用一个变量来标记请求的来源,那实质上就是依赖于开发者自己来判断——这是繁琐而且不可控的。
* 而相比较而言,使用一个简单的映射规则,就能把来自Fragment的请求和来自FragmentActivity自身请求区分开来——十分简单可靠。

6. 总结

  1. 使用startActivityForResult的时候,requestCode一定不要大于0xffff(65535)。
  2. 如果希望在Fragment的onActivityResult接收数据,就要调用Fragment.startActivityForResult,而不是Fragment.getActivity().startActivityForResult。
  3. 看源码果然是学习的好方法~
  4. Google的工程师果然牛逼。

关于作者 :


本文链接:

Android爬坑之旅:软键盘挡住输入框问题的终极解决方案

本文由BarryZhang原创,同时首发于diycode.cc、barryzhang.com 、github.com/barryhappy,非商业转载请注明作者和原文链接。

前言

开发做得久了,总免不了会遇到各种坑。
而在Android开发的路上,『软键盘挡住了输入框』这个坑,可谓是一个旷日持久的巨坑——来来来,我们慢慢看。

入门篇

Base

最基本的情况,如图所示:在页面底部有一个EditText,如果不做任何处理,那么在软键盘弹出的时候,就有可能会挡住EditText。
对于这种情况的处理其实很简单,只需要在AndroidManifest文件中对activity设置:android:windowSoftInputMode的值adjustPan或者adjustResize即可,像这样:

<activity android:name=".MainActivity"
    android:windowSoftInputMode="adjustPan"  >
    ...
</activity>

一般来说,他们都可以解决问题,当然,adjustPanadjustResize的效果略有区别。

  • adjustPan是把整个界面向上平移,使输入框露出,不会改变界面的布局;
  • adjustResize则是重新计算弹出软键盘之后的界面大小,相当于是用更少的界面区域去显示内容,输入框一般自然也就在内了。

↑↑↑ OK,这只是入门,基本上地球上所有的Android工程师都能搞定。
别急,看下面~

加上WebView试试看?坑来了……

上面的入门篇中,软键盘是由原生的EditText触发弹出的。而在H5、Hybrid几乎已经成为App标配的时候,我们经常还会碰到的情况是:软键盘是由WebView中的网页元素所触发弹出的

情况描述

这时候,情况就会变得复杂了:

  1. 首先,页面是非全屏模式的情况下,给activity设置adjustPan会失效。
  2. 其次,页面是全屏模式的情况,adjustPanadjustResize都会失效。

——解释一下,这里的全屏模式即是页面是全屏的,包括Application或activity使用了Fullscreen主题、使用了『状态色着色』、『沉浸式状态栏』、『Immersive Mode』等等——总之,基本上只要是App自己接管了状态栏的控制,就会产生这种问题。

下面这个表格可以简单列举了具体的情况。

表格

为什么说它是个坑?”issue 5497″

上面表格的这种情况并非是Google所期望的,理想的情况当然是它们都能正常生效才对——所以这其实是Android系统本身的一个BUG。

为什么文章开头说这是个坑呢?
——因为这个BUG从Android1.x时代(2009年)就被报告了,而一直到了如今的Android7.0(2016年)还是没有修复……/(ㄒoㄒ)/
可以说这不仅是个坑,而且还是个官方挖的坑~

“issue 5497″,详情传送门 ☞ Issue 5497 – android -WebView adjustResize windowSoftInputMode breaks when activity is fullscreen – Android Open Source Project – Issue Tracker – Google Project Hosting

当然了,不管坑是谁挖的,最终还是要开发者来解决。

遇到坑之后,有两种方法可以过去:躲,或者填。

躲坑姿势

如前文所示,出现坑的条件是:带有WebView的activity使用了全屏模式或者adjustPan模式。
那么躲坑的姿势就很简单了——
如果activity中有WebView,就不要使用全屏模式,并且把它的windowSoftInputMode值设为adjustResize就好了嘛

怎么样,是不是很简单??
20130927092846557

填坑姿势

但总有些时候,是需要全屏模式跟WebView兼得的,这时候,躲坑就不行了,我们需要一个新的填坑的姿势。幸好,开发者的智慧是无穷的,这个坑出现了这么多年,还是有人找到了一些解决方案的。

AndroidBug5497Workaround

我个人认为最好的解决方案是这个:AndroidBug5497Workaround,只需要一个神奇的AndroidBug5497Workaround类。

看名字就知道,它是专门用来对付”5497″问题的,使用步骤也是超级简单:

  1. AndroidBug5497Workaround类复制到项目中
  2. 在需要填坑的activity的onCreate方法中添加一句AndroidBug5497Workaround.assistActivity(this)即可。

经过测试,基本在各个Android版本上都可用,效果基本与设置了adjustResize相当。
看一个对比图:
效果对比图

来自我厂App的某个使用WebView的全屏模式Activity页面,从左到右分别是:没有软键盘的样式、软键盘挡住输入框的效果、以及使用AndroidBug5497Workaround之后的最终效果。

它的原理是什么?

这个炫酷AndroidBug5497Workaround类,其实并不是很复杂,只有几十行代码,先贴在这里:

public class AndroidBug5497Workaround {

    // For more information, see https://code.google.com/p/android/issues/detail?id=5497
    // To use this class, simply invoke assistActivity() on an Activity that already has its content view set.

    public static void assistActivity (Activity activity) {
        new AndroidBug5497Workaround(activity);
    }

    private View mChildOfContent;
    private int usableHeightPrevious;
    private FrameLayout.LayoutParams frameLayoutParams;

    private AndroidBug5497Workaround(Activity activity) {
        FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
        mChildOfContent = content.getChildAt(0);
        mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            public void onGlobalLayout() {
                possiblyResizeChildOfContent();
            }
        });
        frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
    }

    private void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

    private int computeUsableHeight() {
        Rect r = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(r);
        return (r.bottom - r.top);// 全屏模式下: return r.bottom
    }

}

代码大致是做了这么几件事:

1.找到activity的根View

看一下入口的代码:

FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);

其中,第一行中的android.R.id.content所指的View,是Android所有Activity界面上开发者所能控制的区域的根View。

  • 如果Activity是全屏模式,那么android.R.id.content就是占满全部屏幕区域的。
  • 如果Activity是普通的非全屏模式,那么android.R.id.content就是占满除状态栏之外的所有区域。
  • 其他情况,如Activity是弹窗、或者7.0以后的分屏样式等,android.R.id.content也是弹窗的范围或者分屏所在的半个屏幕——这些情况较少,就暂且不考虑了。

我们经常用的setContentView(View view)/setContent(int layRes)其实就是把我们指定的View或者layRes放到android.R.id.content里面,成为它的子View。

所以,然后,第二行content.getChildAt(0)获取到的mChildOfContent,其实也就是用以获取到我们用setContentView放进去的View。

2.设置一个Listener监听View树变化

mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener({ //简化了写法
        possiblyResizeChildOfContent();
});

View.getViewTreeObserver()可以获取一个ViewTreeObserver对象——这个对象是一个观察者,专门用以监听当前View树所发生的一些变化。这里所注册的addOnGlobalLayoutListener,就是会在当前的View树的全局布局(GlobalLayout)发生变化、或者其中的View可视状态有变化时,进行通知回调。

——『软键盘弹出』,则是会触发这个事件的一个源。 (软键盘弹出会使GlobalLayout发生变化)

也就是说,现在能监听到『软键盘弹出』的事件了。

3.界面变化之后,获取”可用高度”

当软键盘弹出了之后,接下来的事情是获取改变之后的界面的可用高度(可以被开发者用以显示内容的高度)。
直接看代码:

    private int computeUsableHeight() {
        Rect rect = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(rect);
        // rect.top其实是状态栏的高度,如果是全屏主题,直接 return rect.bottom就可以了
        return (rect.bottom - rect.top);
    }

View.getWindowVisibleDisplayFrame(Rect rect),这行代码能够获取到的Rect——就是界面除去了标题栏、除去了被软键盘挡住的部分,所剩下的矩形区域——如图所示,红框中的区域。
Rect区域示意图

↑也可以看出:

  • rect.top值,其实就是标题栏的高度。(实际上,这也常常被用作为获取标题栏高度的方法)
  • 屏幕高度-rect.bottom,是软键盘的高度。(获取软键盘高度的方法也出现了)

这时,就有:

  • 全屏模式下,可用高度 = rect.bottom
  • 全屏模式可用高度 = rect.bottom – rect.top

4.最后一步,重设高度

我们计算出的可用高度,是目前在视觉效果上能看到的界面高度。但当前界面的实际高度是比可用高度要多出一个软键盘的距离的。
所以,最后一步,就是把界面高度置为可用高度——大功告成。

    private void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

上面的代码里添加了一个”heightDifference > (usableHeightSansKeyboard/4)”的判断,这是为了去除无谓的干扰。因为能触发OnGlobalLayout事件的原因有很多,不止是软键盘的弹出变化,还包括各种子View的隐藏显示变化等,它们对界面高度的影响有限。加上了这个判断之后,只有界面的高度变化超过1/4的屏幕高度,才会进行重新设置高度,基本能保证代码只响应软键盘的弹出。

总结

总结起来,就是这样:

  1. 普通Activity(不带WebView),直接使用adjustpan或者adjustResize
  2. 如果带WebView:
    • a) 如果非全屏模式,可以使用adjustResize
    • b) 如果是全屏模式,则使用AndroidBug5497Workaround进行处理。

OK,以上就是一段关于『软键盘挡住输入框』的爬坑之旅。


有用的链接:



关于作者 :


从Java到Kotlin:为什么要使用Kotlin进行Android开发?

在使用Kotlin进行了一段时间的Android开发之后,我深深地体验到了它的美好,觉得是时候把它拿出来安利一下了。

1. Kotlin是什么?

Kotlin是一门编程语言,由JetBrains公司开发的。JetBrains就是那个开发了无数个牛逼IDE的公司,Android Studio就是建立在他家的Intellij之上的。 Kotlin是基于JVM的,所以开发者可以什么方便地用它来进行Android开发——支持Kotlin和Java的混合编写。

2. 为什么要使用Kotlin?

我觉得它之所以适合Android开发,主要是因为以下的特点:

2.1 简洁、优雅

写了很多年的Java之后,再写Kotlin,真是觉得如沐春风。 除了不用写分号以及天然支持Lambda表达式之外,它的语法本身就比Java简洁许多。

举几个小例子,比如,类型推导:

val a : Int = 1 // 正常的声明、赋值
val b = 2 // 类型推导,可以省略Int声明 

/* 定义函数:自动推导函数的返回值类型 */
fun sum(a: Int, b: Int) = a + b

字符串模板:

val name = "Barry"
val age = 15
print("My name is ${name}, I am ${age} ")  // 会打印出: My name is Barry, I am 15

下面这个例子,可以对比体会一下他们的不同—— 遍历一个列表,打印其中的奇数。 如果用java写:

List list = Arrays.asList("H",1,3,10,5,9,20,199);
for(Object number : list){
    if(!(number instanceof  Integer)) {
        continue;
    }
    if((Integer)number % 2 == 0){
        System.out.println((Integer)number);
    }
}

而用Kotlin的话,显然会更清晰、简洁、优雅(注意这里的lambda表达式):

val list = Arrays.asList("H", 1, 3, 10, 5, 9, 20, 199)
list.filter { it is Int }
        .filter { it as Int % 2 != 0 }
        .forEach { println(it) }

在Kotlin中,诸如此类的语法糖还有很多:函数默认值、内联函数、ranges、kv遍历Map、lazy属性、数据对象声明、运算符重载…… 如果展开来说的话,每个都可以说很多。

当习惯这些之后,很容易就会有这种感觉:写Kotlin相比较写Java,就像高铁之于绿皮火车、iPhone之于山寨机、4K屏之于800*600……没有它之前觉得老的东西也能用,但是一旦习惯,再改回去的话就觉得浑身难受。

另外,简洁的语法不仅使代码读起来清晰流畅,也让它自然简单易学——基本上花上半天时间,把官方文档看上一遍,就可以开始coding了。

2.2 安全

在Java里,我们常常要进行许多非空判断:

public void fun1 (String str){
   if(str != null) {
       System.out.println("Length = " + str.length());
   }
}

而在Kotlin中,即使不进行这些额外的判断,代码也是健壮的:

// 如果str=null,会打印:Length = null
fun fun1 (str: String?) {
    println("Length = " + str?.length) 
}

还有,Java中经常出现的ClassCastException,在Kotlin中,也可以通过安全的类型转换进行避免:

val l2 = listOf("A",1,3,6,8,'c')
l2.forEach { println(it as? Int) }

2.3 函数、属性扩展

Kotlin提供了一种方法——可以在既不需要继承父类,也不需要使用类似装饰器设计模式的情况下,对类进行扩展。简直是黑科技! 比如,给String类添加一个扩展方法,用以返回它的第一个字符。只需要这样:

// 函数扩展
fun String.firstChar() : String? {
    return if(this.length > 0) this.substring(0,1) else "NULL"
}

这个扩展函数可以在任何地方声明,然后其他任何地方String对象就都可以使用这个方法了:

val s = "Hello".firstChar())  // s的值为"H"
println("你好".firstChar())   // 打印: 你
println("".firstChar())   // 打印: NULL

2.4 兼容Java

一个语言能否被广泛使用,除了它本身要好用之外,还要有一个良好的生态。 尤其在是如今,各种应用层的开发实际上很依赖于各种开源库、第三方组件等,如果一个应用层的语言没有这些支持的话,是很难被广泛使用的。 而对于Kotlin来说,这个问题是不存在的,因为它跟Java是无缝连接的,可以相互操作。 这意味着:

  • Kotlin可以使用所有用Java写的组件:(RxJava、Retrofit、EventBus、Fresco……这些都不是问题!)
  • 在现有的用Java写的项目中可以局部使用Kotlin,混合开发
  • Java->Kotlin,可以逐步迁移而不是非黑即白
  • 使用Kotlin开发时,万一遇到问题,可以瞬间切换回Java~

2.5 工具完善

来,我们再理一下这个关系树:
JetBrains
|–> Intellij IDEA –> Android Studio
|–> Kotlin
Kotlin是JetBrains公司出的;JetBrains向来以做各种牛逼的IDE著称;Android的官方开发工具AndroidStudio就是基于JetBrains公司出品的Intellij改造的。 ↑ 以上是背景。 从实际体验角度来说,Kotlin的开发也是十分方便快捷的。 只需花几分钟给AndroidStudio安装一个Kotlin的扩展插件,就可以迅速开始体验使用Kotlin的快感了。 一开始的时候,你甚至都不需要会使用Kotlin!这是因为Kotlin插件提供了一个Java->Kotlin的转换功能,可以一键把现有的Java代码转换为Kotlin代码,你可以通过阅读它转换后的Kotlin代码,进行学习。
(实际上,我在学习Kotlin的时候,就大量使用了这种方法:当我不知道某个功能用Kotlin怎么写的时候,就会先用Java把它写出来,然后使用转换功能,生成Kotlin代码,从而就知道了Kotlin的写法)

3. 如何开始

因为是一篇纯粹的说明文,所以我把如何开始单独抽离出来,写了这篇文章:《只需五分钟,开始使用Kotlin开发Android》
☞ 链接看这里:
http://www.barryzhang.com/archives/476
http://www.jianshu.com/p/5fa2c1eda64c

其他:

有用的链接们:


关于作者 :
http://www.barryzhang.com
https://barryhappy.github.io
http://www.jianshu.com/users/e4607fd59d0d/latest_articles
http://blog.csdn.net/barryhappy


本文链接:http://www.barryzhang.com/?p=468

只需五分钟,开始使用Kotlin开发Android

1:本文是一篇描述如何在Android上开始一个Kotlin的HelloWorld程序的说明文。
2:其实你如果你网络够给力的话,也许三分钟就可以了。当然网络不够给力,也可能十分钟还没整好~

好了,正文开始:


对于开发者来说,我们正处于一个美好的时代。得益于互联网的发展、工具的进步,我们现在学习一门新技术的成本和难度都比过去低了很多。
假设你之前没有使用过Kotlin,那么从头开始写一个HelloWorld的app也只需要这么几步:

首先,你要有一个Android Studio。

我正在用的是2.2.1版本,其它版本应该也大同小异。

其次,安装一个Kotlin的插件。

依次打开:Android Studio > Preferences > Plugins,然后选择『Browse repositories』,在搜索框中搜索Kotlin,结果列表中的『Kotlin』插件,就是我们要找的目标了。
点击安装,安装完成之后,重启Android Studio。
Kotlin插件

新建一个Android项目

重新打开Android Studio,新建一个Android项目吧,添加一个默认的MainActivity
——像以前一样即可。

Java to Kotlin

安装完插件的AndroidStudio现在已经拥有开发Kotlin的新能力了,那么如何体现这个能力呢?
我们先来尝试它的转换功能:Java -> Kotlin,可以把现有的java文件翻译成Kotlin文件。

打开MainActivity文件,在Code菜单下面可以看到一个新的功能:Convert Java File to Kotlin File。
Java to Kotlin

点击转换,可以看到结果:
java文件:MainActivity.java

package com.barryzhang.kotlinhello;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

转换后的Kotlin文件:MainActivity.kt

package com.barryzhang.kotlinhello

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Kotlin的转换功能是十分实用的,对于我们重用过去的java代码、或者网上搜索到的java代码片段很有帮助。(当然,Kotlin是直接兼容java的,如果不想转换,也可以直接调用Java的方法)

配置gradle文件

MainActivity已经被转换成了Kotlin实现,但是项目目前还不可以用,还需要配置一下,让项目支持grade的编译、运行。
当然,这一步也不需要我们做太多工作——在java转换成Kotlin之后,打开MainActivity.kt文件,编译器会提示”Kotlin not configured”,点击一下Configure按钮,IDE就会自动帮我们配置好了!(所以说Kotlin的工具完善可不是吹的,毕竟Kotlin的老爹JetBrains就是专门做工具的啊)
Kotlin not configured

这个自动配置,实际上是做了这些改动。
项目的build.gradle添加:

buildscript {
    ext.kotlin_version = '1.0.4'
    dependencies { 
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

gradle1

module里的build.gradle添加:

apply plugin: 'kotlin-android'
android { 
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
}
dependencies { 
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
repositories {
    mavenCentral()
}

gradle2

↑↑↑ 熟悉了之后自己手写也是阔以的。

Run

配置之后,等sync完成,就可以运行了~ (如果你sync失败或者耗时过长,赶紧检讨一下自己有没有科学上网?)
hello
biu~起飞,欢迎来到新世界的大门。


Also in :
http://www.barryzhang.com https://barryhappy.github.io http://www.jianshu.com/users/e4607fd59d0d/latest_articles http://blog.csdn.net/barryhappy


本文链接:http://www.barryzhang.com/?p=476