evilyin(寒羽光)的博客
就差一个写代码的了!

面试问题总结之Android篇

handler相关

根据源码里的文档,handler的主要作用有两个:(1)将message和runnable安排在未来的某个时间点执行;(2)将一个操作提交到另一个线程的队列中执行。

handler的原理一直是个麻烦问题,面试经常会问到,而且总是说不好。我很想把他总结出来,每次面试都可以用同一套内容来回答。以下是我的尝试。

handler会关联一个looper,looper和messageQueue是一一对应关系。handler可以通过post()sendMessage()(及各种Delayed、AtTime系列方法)发送消息至messageQueue;looper一直循环从messageQueue里取出message;每个message有一个target,就是发出该message的那个handler,当looper取出message的时候,执行一句:

msg.target.dispatchMessage(msg)

dispatchMessage(msg)实际是调用了target handler里的handleMessage()方法,将这个message给回target handler进行处理。于是最终的处理是在创建该handler的线程中执行的。

举例:更新UI。主线程里创建一个handler实例,将引用传给子线程,子线程完成耗时操作后,使用handler将消息发送至主线程的messageQueue,最后在主线程中执行UI的更新操作。

生命周期相关

生命周期应该是很基础的内容了,但是有些细节我依然很模糊。

  • onPause(), onStop()

源码里的文档说activity只要转入后台就会调用onPause(),在不可见时才会调用onStop()。一般来说onPause()调用之后都会跟着一个onStop()的调用,但是也有的情况是onPause()之后直接到onResume()了。onPause()被执行的可能性是最大的(进程被杀死的时候可能不会调用onStop()onDestroy()),所以在这个方法里可以进行一些状态保存的操作。

  • onSaveInstanceState()

状态保存的官方方法,文档里是这么说的:在activity被杀死以回收系统资源的时候这个方法会被调用,可以保存一些状态用作下次onCreate()或者onRestoreInstanceState()的时候恢复。例如activity A上新开了个activity B,A转到后台,由于要回收系统资源而把A杀死了,那么这个方法会被调用。

文档里还说,不要把这个方法和生命周期里的onPause(), onStop()弄混,有两种情况这个方法不会调用:例如刚才的例子,从B退回到A时,B不会调用这个方法,因为系统认为B的状态不需要保存;或者是B在A前面时,A一直没被杀死,A也不会调用这个方法,因为A的状态一直就在那没变。

  • onNewIntent()

文档只提到activity在把启动模式设成singleTop,或者intent里面带了FLAG_ACTIVITY_SINGLE_TOP的时候,重新启动activity不会新建一个实例而是调用当前实例的onNewIntent()。但是我感觉启动模式是singleTask或者singleInstance的时候也应该是这样的,网上的一些资料也这么说。

  • onConfigurationChanged()

设备的设置改变时会触发,如果manifest里设置了android:configChanges属性,才会调用这个方法,默认情况是会重启activity的,也就是得要重新调用
onCreate()了。

PS:API13以上在设置了权限android.permission.CHANGE_CONFIGURATIONandroid:configChanges="orientation"之后,还是有可能不调用onConfigurationChanged(),需要在android:configChanges里面加一个"screenSize"

singleTask 和 singleInstance

这两个的区别也是一直搞不清楚的问题,综合了文档和一些资料,总结一下:

首先在manifest的activity标签下还可以设置一项属性叫做taskAffinity,用来标识activity属于哪个task。默认情况下taskAffinity是应用的包名。

设置了singleTask的activity,如果没有设置taskAffinity,那么启动时还是会在原本的task当中,因为他会去找跟它的taskAffinity相同的task,如果存在这样一个task,就在这个task中启动,如果不存在这样一个task,才创建一个新的task,并在新task中启动。如果已经存在这个activity的实例,那么会调用onNewIntent()

singleInstance和singleTask只有一点不一样,设置了singleInstance的activity启动时必然会在新的task当中,而且这个task只会有这一个activity存在。没设置taskAffinity的话,在他之上新建的activity会回到之前的task当中,设置了taskAffinity的话,新建的activity会再另起一个task。

这部分参考了http://blog.csdn.net/linmiansheng/article/details/24297375

点击事件的传递

故事开始于activity的dispatchTouchEvent()方法,activity收到触摸事件以后调用这个方法进行处理,把事件向下分发给各级view。在activity的这个方法中,先会尝试调用window.superDispatchKeyShortcutEvent()将事件向下分发。如果下面的view有人处理了该事件,则返回true;如果都不处理,则会调用自己的onTouchEvent()方法,返回该方法调用结果。

继续往下,一般来说下面应该是一个layout了,也就是ViewGroup。ViewGroup里的dispatchTouchEvent()源码特别长,大概内容是调用了自己的onInterceptTouchEvent()方法,查询是否要拦截事件(调用之前还先查了FLAG_DISALLOW_INTERCEPT看是不是允许拦截),根据是否拦截进行不同处理:

  • 如果不拦截,继续往下分发;
  • 如果拦截,那么由自己来处理,进入处理流程:首先看自己有没有设置onTouchListener,有的话调用之,没有的话则轮到自己的onTouchEvent()方法,其中还会再看有没有设置onClickListener,有的话调用之。

如果再往下分发到一个view当中了,view是没有onInterceptTouchEvent()的,他的dispatchTouchEvent()只会分配给自己处理,处理过程和ViewGroup的处理流程一样。

如果ViewGroup的某个childView的dispatchTouchEvent()返回了true,那么ViewGroup会把他设为target,后续的事件都会直接交给他处理,不用一个个调用所有child的dispatchTouchEvent()方法了。

如果view的onTouchEvent()返回false了,那么dispatchTouchEvent()会向上层返回false表明自己没有处理该事件,上层ViewGroup按照处理流程来处理。如果上层的ViewGroup处理流程也返回false,那么会一级一级向上直到activity那里。

view的重绘

<未完待续>

面试问题总结之计算机基础篇

计算机网络

  • TCP的建立连接和断开链接

为什么要用三次握手,这是被问到过的问题,我没答好。

数据库

数据库事务是指作为单个逻辑单元执行的一系列操作。一个逻辑单元要成为事务,必须满足四个属性:ACID。

A:Atomicity 原子性,要么全都执行,要么全都不执行;

C:Consistency 一致性,指一个事务在执行前和执行后,数据库都必须保持一致状态;

I:Isolation 隔离性,由并发事务做的修改必须与其他并发事务做的修改隔离;

D:Durability 持久性,事务完成之后,对系统的影响是永久性的。

<未完待续>

面试问题总结之Java基础篇

HashMap、Hashtable

Java文档里提到的Hashtable和HashMap的区别:一是Hashtable是同步的,二是HashMap可以接受null作为key和value。Hashtable的同步机制实际并不能保证线程安全,有多线程操作时应当选用ConcurrentHashMap。

还有一些资料列举出来的区别:

  • 数据遍历方式,Hashtable有Iterator和Enumeration,HashMap只有Iterator;
  • Hashtable和HashMap用Iterator遍历时都支持fast-fail机制,但是Hashtable用Enumeration遍历时不支持fast-fail;
  • HashMap的根据hash值计算数组下标的算法要优于Hashtable,通过对Key的hash做移位运算和位的与运算,使其能更广泛地分散到数组的不同位置;
  • Entry数组的长度:
    • Hashtable 缺省初始长度为11,初始化时可以指定initial capacity
    • HashMap 缺省初始长度为16,长度始终保持2的n次幂,初始化时可以指定initial capacity,若不是2的幂,HashMap将选取第一个大于initial capacity 的2的n次幂值作为其初始长度。

GC机制

这东西有点复杂,不敢说懂了多少……只能先记录一些自己的理解。主要参考的是Java编程思想的第5章。

首先GC有两个关键问题,第一是如何确定某个对象需要被回收,第二是回收的具体算法。

  1. 确定某个对象是垃圾:

    最直接的方法是引用计数法,每个对象都有一个引用计数器,当有一个引用连接到对象上时,计数器加1。当引用离开作用域或被置为null时,计数器减1。但是这种办法不能解决循环引用的问题。

    为了解决循环引用,引入了另一种办法,主要思想是:对任何有用的对象,一定能追溯到其存活在堆栈或静态存储区中的引用。因此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。

  2. 垃圾回收算法:

    参考:

    http://www.cnblogs.com/dolphin0520/p/3783345.html

    • “标记-清扫”:通过上面方法找出所有存活对象,每找到一个就给对象设一个标记,当全部标记完成的时候开始清理,没有标记的对象被释放。这种方法会造成可用内存空间不连续,因此需要清理完以后再进行整理,也就是“标记-整理”。

    • “停止-复制”:将所有存活对象从当前堆复制到另一个堆,没有复制的都是垃圾,然后把当前堆的内存全部清理掉。这种方法会把内存分为两个堆,相当于降低了一半的内存使用率;同时如果产生的垃圾很少,那么每次都要复制大量对象到另一处,会很浪费。

    • 自适应的分代的算法:主流采用的算法,根据对象存活时间将内存分为“新生代”、“老年代”,新生代里存放生命周期短的临时性对象,老年代存放稳定的长期对象。新生代每次回收都要回收大量垃圾,适合用“停止-复制”算法;老年代回收的垃圾较少,适合用“标记-清扫”算法。这就是一种“自适应”技术。(另外有一种说法是把堆区外的方法区定义为“永久代”,现在好像要改变这个概念,待后续研究。)

      http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html

      这篇提到了很多内存分配相关的内容,因此也放上来作为参考。

集合类

Java集合类归根结底有三种,List,Map,Set。Java编程思想上把Queue也作为一个基本的集合类,其实我不太懂为什么。Queue和Stack都是由LinkedList实现的,应该也算是从List衍生出去的吧,不懂为什么要和List分开。

  • 扩容问题

ArrayList扩容时,是用Arrays.copyOf()方法,传入原数组和新容量大小,返回一个新的数组。再查看copyOf方法时,是调用的System.arraycopy()方法。这个方法是一个native的方法,看不到源码了……

扩容时新容量是max{原数组容量的1.5倍,传入的参数}。

<未完待续>

拓扑排序解决图中是否存在环路问题

首先是前段时间网易游戏在线笔试的一道编程题:源代码编译

在leetcode上有一道几乎是一模一样的题目:207. Course Schedule

大意都是一个图中的某两个节点存在先后顺序,要求判断图中有没有环路。leetcode的提示说的也很明确:这是一个拓扑排序(Topological sort)的问题。

拓扑排序的定义:设有向图G中节点u和节点v之间存在有向边u->v,那么对节点排序的结果中,u始终在v的前面。

从定义中很容易得出,如果G中同时存在有向边u->v和v->u,也就是存在一个环路,那么u和v就无法确定谁在谁前面,也就是G无法被拓扑排序。因此,一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。

数据结构教科书上有拓扑排序的典型算法,我摘录了一下网上对这个算法的总结:

1
维护一个入度为0的顶点的集合:
每次从该集合中取出(没有特殊的取出规则,随机取出也行,使用队列/栈也行,下同)一个顶点,将该顶点放入保存结果的List中。
紧接着循环遍历由该顶点引出的所有边,从图中移除这条边,同时获取该边的另外一个顶点,如果该顶点的入度在减去本条边之后为0,那么也将这个顶点放到入度为0的集合中。然后继续从集合中取出一个顶点…………
当集合为空之后,检查图中是否还存在任何边,如果存在的话,说明图中至少存在一条环路。不存在的话则返回结果List,此List中的顺序就是对图进行拓扑排序的结果。

对于leetcode的这道题,实现这个算法的代码如下:

public boolean canFinish(int numCourses, int[][] prerequisites) {

    // 用矩阵保存题目中的数据,matrix[i][j]表示i->j的有向边
    int[][] matrix = new int[numCourses][numCourses]; 
    // 记录每个顶点的入度
    int[] indegree = new int[numCourses];

    // 初始化
    for (int i=0; i<prerequisites.length; i++) {
        int ready = prerequisites[i][0];
        int pre = prerequisites[i][1];
        if (matrix[pre][ready] == 0)
            indegree[ready]++; // 避免重复计算相同的边
        matrix[pre][ready] = 1;
    }

    int count = 0; // 记录出队的顶点个数
    Queue<Integer> queue = new LinkedList();
    for (int i=0; i<indegree.length; i++) {
        if (indegree[i] == 0) queue.offer(i); // 入度为0的顶点入队
    }
    while (!queue.isEmpty()) {
        int course = queue.poll(); // 顶点course出队
        count++;
        for (int i=0; i<numCourses; i++) {
            if (matrix[course][i] != 0) { // 如果存在course->i的边
                if (--indegree[i] == 0) 
                    queue.offer(i);
                    // 顶点i的入度减一,如果入度变为0,顶点i入队
            }
        }
    }
    // 所有顶点均出队,说明排序完成,反之未完成,存在环路
    return count == numCourses;
}

上面的算法是基于BFS的,而用DFS同样能做拓扑排序,只不过和BFS正好相反,DFS的算法是维护一个出度为0的顶点集合。由于DFS使用了递归,需要用一段伪代码来描述一下:(来自维基百科)

1
L ← Empty list that will contain the sorted nodes
S ← Set of all nodes with no outgoing edges
for each node n in S do
    visit(n) 
function visit(node n)
    if n has not been visited yet then
        mark n as visited
        for each node m with an edge from m to n do
            visit(m)
        add n to L

根据这个思路,在BFS的代码上做一些改动可以得到DFS的代码:

public boolean canFinish(int numCourses, int[][] prerequisites) {
    int[][] matrix = new int[numCourses][numCourses];
    boolean[] visited = new boolean[numCourses];
    for (int i=0; i<prerequisites.length; i++) {
        int ready = prerequisites[i][0];
        int pre = prerequisites[i][1];
        matrix[pre][ready] = 1;
    }
    for (int i=0; i<numCourses; i++) {
        if (!visit(matrix, visited, i)) return false;
    }
    return true;
}

private boolean visit(int[][] matrix, boolean[] visited, int course) {
    if (visited[course]) return false;
    else {
        visited[course] = true;
        for (int i=0; i<visited.length; i++) {
            if (matrix[i][course]!=0) {
                if (!visit(matrix, visited, i)) return false;
                // 如果存在i->course的边,对i递归调用visit方法
                // visited.length(numCourses)太大时会超时
            }
        }
        visited[course] = false;
        return true;
    }
}

然而上面这段代码在leetcode上是会超时的,可能是因为visit函数里的for循环体太长太暴力了,测试的时候numCourses=1000就不行了。可以考虑的优化方式是采用邻接表代替矩阵存储图的数据,这样的话i->course的边的数量可以确定为邻接表的长度,就不需要暴力搜索整个numCourses的长度了。

AlphaGo核心算法之一——蒙特卡洛树搜索

近来全球都在关注的一大热点就是谷歌的人工智能围棋程序AlphaGo对阵围棋职业九段李世石的人机大战了,在3月9号的第一场比赛里AlphaGo就取得胜利,可以说轰动了全世界。(这篇文章还没写完,第二盘AlphaGo又赢了。)虽然人工智能对于我这样的初初初级程序员来说有点遥不可及,但是对这样重大的技术突破保持好奇心和求知欲应该也是程序员要具备的素质吧……本着这样的想法,我在网上搜了些资料,了解了一下AlphaGo使用的核心算法之一——蒙特卡洛树搜索。水平所限,只能写一点自己的理解,肯定有很多不对的地方。

蒙特卡洛算法

首先从蒙特卡洛算法说起,这个算法其实不是什么新技术,是上世纪40年代美国在第二次世界大战中研制原子弹的“曼哈顿计划”计划的成员S.M.乌拉姆和J.冯·诺伊曼(计算机之父!)首先提出的。主要思想是通过大量随机样本的概率分布来得到所要计算的值。用它最经典的应用来举例——计算圆周率:

正方形内切圆,圆与正方形面积比是π/4:


在正方形内部随机产生10000个点,通过点的坐标(x,y)计算点与圆心的距离,从而判断该点是不是在圆的内部。

如果这些点均匀分布,那么圆内的点应该占到所有点的 π/4,因此将这个比值乘以4,就是π的值。

对这个例子进行推广,我们可以计算任意一个积分的值,例如计算函数y = x²在 [0, 1] 区间的积分,就是求出下图红色部分的面积。

这个函数在 (1,1) 点的取值为1,所以整个红色区域在一个面积为1的正方形里面。在该正方形内部,产生大量随机点,可以计算出有多少点落在红色区域(判断条件 y < x2)。这个比重就是所要求的积分值。

蒙特卡洛树搜索

蒙特卡洛树搜索(Monte-Carlo Tree Search,MCTS)是用于人工智能选择最优解的算法,典型应用是在博弈游戏中。基本思想也很简单,以围棋为例:

建立一个搜索树,节点是当前的局面,节点的子节点是下一步棋后形成的局面,根节点就是一无所有的空棋盘。首先在根节点中随机选择一个子节点,相当于在棋盘中随机下了一步棋,然后子节点继续随机选择子节点,直到叶子节点也就是分出胜负的状态。如果胜了,那把刚才路径上的所有节点权重都加1,这样下次随机时有更大的可能会随机到这些点。

上面的过程重复无数次,越是有可能胜利的下法,权重就会越来越高,最后选择胜率最大的那个方案落子。这个时候,这一步棋才真正下到棋盘上。

当然,如果完全按照这个原始的算法,要搜索的数量级实在太大了。围棋有19x19=361个点,那么搜索树的节点至少也是361!个,数量级大概是10的170次方,已经超过了宇宙中所有原子的数量。因此AlphaGo还采用了更加复杂的方法对搜索树进行剪枝。具体的内容无法在这里展开了,毕竟我只是关注算法……

最后附上链接,其中左右互搏,青出于蓝而胜于蓝?—阿尔法狗原理解析这篇文章比较清晰易懂,可以一看,感觉How AlphaGo Works这篇写的还不如前者好懂。

参考链接:

蒙特卡罗方法入门

Monte Carlo Tree Search

左右互搏,青出于蓝而胜于蓝?—阿尔法狗原理解析

How AlphaGo Works 英文,里面有中文翻译的链接。

AlphaGo项目主页 其中有nature上发表的那篇论文的链接。

转一篇正则表达式入门教程

号称30分钟入门正则表达式,不得不说我看过以后确实入了一点点门。

http://deerchao.net/tutorials/regex/regex.htm

据说有人面试的时候遇到过用正则表达式匹配手机号的问题,我用刚入门的水平试写了一个,自己测试了下应该没啥问题:^1[3|4|5|7|8]\d{9}$

从一道算法题看Integer类型的“常量池”

原题:https://leetcode.com/problems/min-stack/

题目本身不难,要求实现一个栈,除了有push(),pop(),top()三个基本功能外,还能够在常数时间复杂度内获得栈内的最小值。

思路很容易,维护两个栈,一个栈保存正常的数据,另一个栈的栈顶一直保持是当前的最小值,代码如下

class MinStack {

    Stack<Integer> stack = new Stack<>();
    Stack<Integer> minStack = new Stack<>();

    public void push(int x) {
        stack.push(x);
        if (minStack.size()==0 || x<=minStack.peek()) {
            minStack.push(x);
        }
    }

    public void pop() {
        if (stack.pop()==minStack.peek()){
            minStack.pop();
        }
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

然而,wrong answer了。出错的测试用例是这样:

push(512),push(-1024),push(-1024),push(512),pop,getMin,pop,getMin,pop,getMin

正确的getMin输出结果应该是[-1024,-1024,512],但是我的结果是[-1024,-1024,-1024]

百思不得其解,代码里怎么都看不出问题在哪,不得已看了别人的答案,思路一模一样的答案里唯一不同的地方在:

public void pop() {
    int x = stack.pop();
    if (x==minStack.peek()){
        minStack.pop();
    }
}

我猜想应该是在==上面出了问题。直接调用pop()的话,返回的是一个Integer类型的对象,而正确的答案里是把返回的对象赋给了一个int变量。难道Integer类型互相之间不能用==来比较吗?但是其他的测试用例为什么又能通过呢?

经过进一步的尝试,我发现数值大于某个数的时候==就不能用了,小于的时候==能正常用,这又是为什么?

网上搜了一圈,终于搞清楚了,在Integer类的源码里有这么一段:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

可以看到有一个if判断,IntegerCache.low是写死的-128,IntegerCache.high是可以根据JVM的设置变化的,默认是127。也就是说在-128<=i<=127的范围内,取的是IntegerCache.cache这个数组里的东西,在范围以外就是一个new Integer(i)了,当然就不能用==来比较了。这个IntegerCache.cache就是传说中的常量池。网上有一段对于他的解释:

看一下IntegerCache这个类里面的内容:

private static class IntegerCache {

    private IntegerCache() {

    }

    static final Integer cache[] = new Integer[-(-128) + 127 + 1];

    static {

        for (int i = 0; i < cache.length; i++)

        cache[i] = new Integer(i - 128);

    }

}

由于cache[]在IntegerCache类中是静态数组,也就是只需要初始化一次,即static{……}部分,所以,如果Integer对象初始化时是-128~127的范围,就不需要再重新定义申请空间,都是同一个对象—在IntegerCache.cache中,这样可以在一定程度上提高效率。

回到开头的题目,我们不但可以用上面提到的答案里的写法,也可以用Java里比较两个对象的方法equals(),代码可以这样写:

public void pop() {
    if (stack.pop().equals(minStack.peek())){
        minStack.pop();
    }
}

这回终于accepted了。

还是要多提高自己的姿势水平啊!

一道算法题

原题链接:https://leetcode.com/problems/product-of-array-except-self/

题目大意:给一个数组,返回一个数组,要求返回的数组的第i个元素是原数组除了第i个元素的其他所有元素乘积。

例如给[1,2,3,4], 返回[24,12,8,6]

很容易想到先把所有数乘起来,然后每扫描到第i个元素时就用总乘积除以元素i,然而题目说了:不许用除法!另外还要求能否用常数级别的空间复杂度完成,返回的数组不算空间复杂度。

看到这种题目我总是会用一种类似动态规划的思想去考虑,就是先对原始数据做一些处理,前面的处理结果可以用于后续的处理。比如一开始的思路“把所有数乘起来”就是一个处理方式。这题虽然不许用除法,但是思路还是可以继续往这边想。
如果把原有数组里的数一个个乘起来,每乘到第i个元素时,就得到从0到i的所有元素乘积;那怎样获得从i到末尾的所有元素乘积呢?倒着再乘一遍就好了!从末尾开始一个个乘,每到i时就得到了末尾到i的所有乘积了。至于去掉元素i,只要乘的时候少乘一个数,扫描到i时乘到i-1就好了。

于是代码如下:

public int[] productExceptSelf(int[] nums) {
    int[] result = new int[nums.length];
    int temp = 1;
    for (int i=0; i<nums.length; i++) {
        if (i==0) {
            result[i]=1;
        }else {
            result[i]=result[i-1]*nums[i-1];
        }
    }
    for (int j=nums.length-1; j>=0; j--) {
        if (j!=nums.length-1) {
            temp *= nums[j+1];
            result[j] *= temp;
        }
    }
    return result;
}

后来看了别人的代码,思路一模一样,但是人家的写法不需要在i==0j==nums.length-1的时候做特别判断,果然自己在写代码的优雅程度上还是不够啊……

扩展TextWatcher实现监听EditText字数

在Android开发中,有时会需要监听某个EditText中已输入的字数,达到要求后触发某个行为,例如账号或者密码输入框限定最少输入6位英文或数字,达到6位后登录按钮才变为可用。Android提供了一个监听EditText的接口TextWatcher,通过调用EditText里的addTextChangedListener()方法实现监听。TextWatcher里面有三个要实现的方法,我试着根据源码里的注释解释一下三个方法的作用:

public interface TextWatcher extends NoCopySpan {

// 该方法被调用时说明在s中,从下标start开始的count个字符将要被替换为长度为after的字符串
    public void beforeTextChanged(CharSequence s, int start, int count, int after);

// 该方法被调用时说明在s中,从start开始的count个字符刚刚替换掉了长度为before的字符串
    public void onTextChanged(CharSequence s, int start, int before, int count);

// 该方法被调用时说明在s中的某个地方发生了字符变化
    public void afterTextChanged(Editable s);
}

我们要达到的目的是,字符数量满足要求以后执行某操作,字数不够时该操作变为不可执行,因此只需要重写afterTextChanged(Editable s)这个方法就好了。于是我封装了一个抽象类,留出两个抽象方法供使用者实现业务逻辑:

/**
 * @author evilyin(ChenZhixi)
 * @since 15/10/31
 */
public abstract class TextCountWatcher implements TextWatcher {
    //要求的字数
    private int count;

    public TextCountWatcher(int count) {
        this.count = count;
    }
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        if (s.length() >= count) {
            onTextMatchCount();
        }else if (s.length() < count) {
            onTextLessThanCount();
        }
    }

    // 当字数达到或超过要求时触发
    public abstract void onTextMatchCount();

    // 当字数不够时触发
    public abstract void onTextLessThanCount();
}

实际使用的例子,mobileNumber是输入手机号的EditText,必须输入11位,才可以点击边上的按钮:

mobileNumber.addTextChangedListener(new TextCountWatcher(11) {
    @Override
    public void onTextMatchCount() {
        canClick();
    }

    @Override
    public void onTextLessThanCount() {
        notClick();
    }
});

有的时候我们还想实现另一个功能,在某个输入框中输入的字数达到上限后就不能输入了,这同样可以通过TextWatcher实现。下面的代码在参考了网上的写法后自己做了些改进:

一篇很好的介绍gradle的文章

原文地址:http://www.flysnow.org/2015/03/30/manage-your-android-project-with-gradle.html

根据这篇文章搞定了多渠道打包以及自定义打包apk文件名的问题。

自己搞的时候还碰到点小问题:

比如在设置文件名的时候,applicationVariants.all 后面的那部分代码我最初是根据文章中写的放在buildTypesreleasedebug下面各一份,然而打debug包的时候并没有起作用。后来看了另外一篇文章,把这部分直接放在android下面,就有效了。

对了,这篇文章的博客也用的hexo的默认模板……跟我这里一模一样。

另外还有关于applicationId这个的解释,这篇文章:http://blog.csdn.net/maosidiaoxian/article/details/41719357
是翻译的Android官方文档,说的挺清楚的。