且听风吟

Don't panic! I'm a programmer.

Android模拟器与host主机以及模拟器之间进行TCP通信

| Comments

QEMU Networking

Android 模拟器基于QEMU虚拟机构建,虚拟机(guest)与所在的PC主机(host)之间的网络通信有几种方式:

  1. NAT方式
    Android模拟器的网络连接使用NAT方式。
    这种方式通过host机器的网络接口访问网络,优点是方便设置,不用创建额外的网络接口。缺点是guest可以访问外部网络,但是反过来host不能访问到guest。Android通过端口转发解决这一问题。
    NAT网络结构如下所示(图片来自qumu wiki):

    /images/qemu_Slirp_concept.png

    由此可见,guest与host不在同一个网段,guest通过10.0.2.2访问host机器.

  2. Bridge方式

模拟器与host通信

启动模拟器,adb server会在5554端口上监听来自adb client的连接,我们可以通过这个端口与模拟器通信。
在模拟器上运行如下服务器程序,在模拟器的8000端口上监听客户端连接,接收到客户端连接后,发送一段echo字符串,然后关闭连接:

public class EmulatorServerActivity extends Activity {
    private ServerSocket serverSocket = null;

    public static final int SERVERPORT = 8000;

    private boolean keepRunning = false;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        keepRunning = true;
        new Thread(new ServerThread()).start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        try {
            keepRunning = false;
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    class ServerThread implements Runnable {
        public void run() {
            try {
                serverSocket = new ServerSocket(SERVERPORT);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            while (keepRunning) {
                try {
                    Log.d("Server", "Wating for connection...");
                    Socket s = serverSocket.accept();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(
                            s.getInputStream()));
                    String msg = reader.readLine();
                    Log.d("Server", "Received message from client:" + msg);
                    String echo = "You are telling me: " + msg + ", bye\n";
                    s.getOutputStream().write(echo.getBytes());
                    s.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在host机器上通过telnet连接到模拟器:

telnet localhost 5554

然后设置端口转发:

redir add tcp:7777:8000
redir list // 显示已添加的端口转发
exit

添加端口转发命令格式为:

redir add <protocol>:<host-port>:<guest-port>

其中,protocol必须是tcp或udp,host-port是主机上开启的端口号,guest-port 是模拟器的监听端口号。

这条端口转发命令表示将发送到host机器的7777端口的所有tcp数据转发到guest机器的8000端口,也就是我们的server程序监听的端口,这个过程对client来说是完全透明的,它只知道它把数据发送到host的7777端口,它对数据如何转发一无所知。这时在host机器上可以看到7777端口已经处于监听状态:

$ netstat -tln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               LISTEN 

通过设置端口转发,host机器就可以通过这个7777端口连接guest机器上的服务程序了:

$ telnet localhost 7777
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
You are telling me: hello, bye
Connection closed by foreign host.

模拟器之间通信

启动另一个模拟器,运行客户端程序:

public class EmulatorClientActivity extends Activity {
    private String serverIpAddress = "10.0.2.2";

    // 注意:我们需要连接host机器的7777端口
    private static final int REDIRECTED_SERVERPORT = 7777;

    private Socket socket;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        new Thread(new CommsThread()).start();
    }

    class CommsThread implements Runnable {
        public void run() {
            try {
                InetAddress serverAddr = InetAddress.getByName(serverIpAddress);
                socket = new Socket(serverAddr, REDIRECTED_SERVERPORT);
                socket.getOutputStream().write(new String("Hello\n").getBytes());
                Log.d("Client", "Message sent to server");
                socket.close();
            } catch (UnknownHostException e1) {
                e1.printStackTrace();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

注意:在这个客户端程序里,我们需要连接的socket为10.0.2.2:7777,其中,10.0.2.2是host机器的ip地址,发送到host机器的7777端口的数据会被透明的转发到另一个模拟器的8000端口上,这样,连接就建立起来了。

Reference

Git Tips

| Comments

配置

  • git alias

    git config —global alias.co checkout
    git config —global alias.br branch
    git config —global alias.ci commit
    git config —global alias.st status
    … …

查看历史

  • git log

    -N 只显示最近N次提交
    -p 按补丁格式显示每个更新之间的差异。
    —stat 显示每次更新的文件修改统计信息。
    —shortstat 只显示—stat中最后的行数修改添加移除统计。
    —name-only 仅在提交信息后显示已修改的文件清单。
    —name-status 显示新增、修改、删除的文件清单。
    —relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
    —graph 显示 ASCII 图形表示的分支合并历史。
    —pretty 使用其他格式显示历史提交信息。可用的选项包括oneline,short,full,fuller和format(后跟指定格式)。
    —since,—after 仅显示指定时间之后的提交。
    —until,—before 仅显示指定时间之前的提交。
    —author 仅显示指定作者相关的提交。
    —committer 仅显示指定提交者相关的提交。

    git log --stat
    查看每次提交修改的代码行数

    git log --name-status --author=bob --pretty=format: // 注意format为空 统计bob改动的文件列表,可以将结果保存为文本文件,用vim打开,使用:sort u删去重复的行

    git instaweb 以web的方式查看仓库
    git instaweb --stop 停止服务器

撤销回滚

  • git reset

    1. git reset —soft 只撤销 commit,保留 working tree 和 index file。
    2. git reset —hard 撤销 commit、index file 和 working tree,即彻底销毁最近一次的 commit
    3. git reset —mixed 撤销 commit 和 index file,保留 working tree
    4. git reset 和 git reset —mixed 完全一样
    5. git reset —用于删除登记在 index file 里的某个文件。

    git reset的第二种用法:
    git reset [-q] [<commit>] [--] <paths>... 将指定路径的文件在commit的内容拷贝到暂存区。git reset path和git add path效果相反。

  • 回退版本(参考这里)

    回退版本很简单,git checkout sha1 即可。
    现在你不在任何一个branch(no branch),执行git status显示:

      $ git status
      # Not currently on any branch.
      nothing to commit (working directory clean)
    

    这个操作会将HEAD指向指定commit,并重置index和working tree到此commit的状态. 但由于此commit不是任何branch的头(“head” or “tip”),所以git会给出警告.这种情况称为”HEAD is detached”.参考这里
    虽然警告,但并没有什么危险,只要别做commit,最后checkout回(某个branch)来就行了.
    如果此时做commit呢?—那也没有什么,只是这个新的commit以后只能通过SHA1来引用了(当然,还可以用git reflog看到),好比一个野指针;或者你赋个tag给它.不过这没有意义,如果你真的想从这儿发展下去,那么应该新建一个branch,比如git checkout -b <new_branch> <start_point>
    但是注意,做此操作时working tree和index应该是”干净的”,就是和HEAD(所指的commit)完全一致,没有任何改动,否则git会报错不执行;你可能会看到类似这样的error:

      error: You have local changes to 'somefile'; cannot switch branches.或
      error: Entry 'somefile' would be overwritten by merge. Cannot merge.
    

    (不过,你可以用—merge选项强制执行,此时git会做一个three-way merge)

    要切换回HEAD commit,执行git checkout branch-name

Diff & Patch

  • git diff

    git diff 查看 working tree 与 index file 的差别。  
    git diff --cached/--staged 查看 index file 与最后一次提交的commit之间的差别的。  
    git diff HEAD 是查看 working tree 和最后一次提交的commit之间的差别的。  
    
  • git format-patch -1    生成最后一个提交对应的patch文件
    git am < diff.patch    把一个patch文件应用到当前分支
    

Branch

  • 所谓Branch,本质上是一个指针(一串SHA号),指向某个commit。特别的,HEAD是当前分支的最后一个commit的别名。
  • git merge 多个分支进行merge时可能存在两种方式:

    1. Fast-forward
    2. 两个分支(A,B)的末端和这两个分支的共同祖先(C)进行三方合并(可能出现冲突),合并后将产生一个新的commit,这个commit有两个祖先(A和B)。

    git branch --merged 查看有哪些分支已经合并进当前分支了,这些分支可以删掉了,因为这些分支的更改已经包含再当前分支里面了
    git branch --no-merged 查看尚未合并的分支

  • git rebase A—-B—-C topic / D—-E—-F—-G master 当前分支为topic,git rebase master,rebase的过程是:

    1. 把从topic和master共同的祖先E开始在当前分支上的提交生成一个patch文件
    2. 将A、B、C砍掉,相当与执行git reset --hard master
    3. 将生成的patch文件一个一个应用到G上

    结果变成这样: A’—B’—C’ topic / D—-E—-F—-G master topic的提交历史被改写了,成了master分支的直接下游,这时在master分支上做一次fast-forward的merge就好了。

    可见,相比于merge,rebase不会产生一个新的commit,合并后的主线更加干净。如果希望在执行git pull时自动使用git rebase取代默认的git merge操作,可以执行

    git pull --rebase
    

    参见这里

  • git pull

    git pull = git fetch + git merge
    git pull origin next = git fetch origin/next + git merge origin/next // 将远程分支next合并到当前分支上

    git pull会把所有正在跟踪远端仓库的分支的本地分支进行数据拉取。 执行git pull, git merge, git reset之前的HEAD会被保存到ORIG_HEAD,所以如果要回滚刚才的pull动作,那么执行git reset --hard ORIG_HEAD。查看pull下来的内容,执行git diff ORIG_HEAD

  • git push

    git push origin featureA 将本地的featureA分支推送到远程仓库
    git push origin local_branch:remote_branch 将本地的local_branch的内容推送到origin远程仓库的remote_branch分支上去,所以执行 git push origin :remote_branch 将删除远程版本库的 remote_branch 分支。

  • git fetch

    $ git remote add rc git@github.com:calvinlee/git-sandbox.git
    $ git remote -v
    origin    git@github.com:calvinlee/git-sandbox.git (fetch)
    origin    git@github.com:calvinlee/git-sandbox.git (push)
    rc    git@github.com:calvinlee/git-sandbox.git (fetch)
    rc    git@github.com:calvinlee/git-sandbox.git (push)
    $ git fetch rc
     * [new branch]      experimental -> rc/experimental
     * [new branch]      master     -> rc/master
    

    运行git fetch后,本地多了从rc这个库fetch下来的分支:

    $ git branch -r
    origin/HEAD -> origin/master
    origin/experimental
    origin/master
    rc/experimental
    rc/master
    

    现在可以切换到这个分支查看,或者选择merge到本地分支。(远程分支以这样的形式命名:远程仓库名/分支名)

git reset与git checkout

git reset用来修改refs的指向:修改的是.git/refs/heads/master的值,HEAD本身一直指向refs/heads/master, 并没有在reset过程中改变。 git checkout用来修改HEAD的指向: 修改的是.git/HEAD的文件内容, checkout不影响.git/refs/heads/下文件的内容。

什么叫detached HEAD?
这个状态下.git/HEAD指向了一个具体的commit ID,而不是一个分支的名称。

符号

  • branchA..branchB

    用来获取在branchB上但是不在branchA上的提交。如:
    git log --pretty=format:"%h %s" HEAD~3..HEAD 显示最近三次提交的log
    git log origin/master..HEAD 显示所有在当前分支上但不在远程分支上的提交。如果当前分支正在track远程分支,那么下次运行git push将推送这些提交到远程分支上。

    这种表示法与下列语法等价:

     ^branchA branchB
     branchB --not branchA
    
  • branchA…branchB

    获取被两个分支之一包含但不被这两个分支同时包含的提交。

  • ^[N] 和 ~[N]

    两个分支合并后,你合并时所在分支称为first parent,你所合并的分支称为second parent
    HEAD^[N]表示HEAD的第N个second parent
    HEAD~[N]表示HEAD的第N个first parent

  • 获取某一次特定提交对应的文件内容 e43fef:path/to/file
    获取暂存区中的文件对象:
    :path/to/file

其它

  • 将多个commit合成一个commit后提交

    场景:
    packages/apps/Email 已经提交了多个commit在branchA上,现在要把这些commit压成一个commit合并到merge_fix分支上。

     $ cd packages/apps/Email
     $ repo sync .
     $ git log --pretty=oneline // 假如需要squash commit1 和 commit2
       e8949d30bcb568b32a12338a28fb9d7a13e8b086 commit1
       6d7eef22cd2634bd0da8de8e38635dd92b3e5a48 commit2
       0130c5a03756b01ec4a39422ab08474625153a81 commit3
     $ git rebase -i 0130c5a03756b01ec4a39422ab08474625153a81 //注意这里是想要squash的commit的前一个commit(commit3), 更改commit message后继续
     $ git log -2 //找到squash后的commit,假如squash后的commit号为bed1b4fa7ae3f9a4a546f2aa367a8f2e7ba4b7b1
     $ git checkout merge_fix
     $ git cherry-pick bed1b4fa7ae3f9a4a546f2aa367a8f2e7ba4b7b1
    

    完毕

  • 一个图片文件 index.jpeg 冲突了,如何取出他人的版本?

    git show :3:./index.jpeg > index.jpeg-theirs
    

    可以先使用git ls-files查看:

    $ git ls-files -s
    100644 a97de6ee1b2352f4e33d09196fa478ffaae0b024 0 README
    100644 04c14c2023f69123e719fae7c4242537c943b726 0 file1
    100644 2f08be9a02925b5c016904e19fbd5e8d057ae756 0 hello.c
    100644 eb651d4b863843d3ad12c446b541075f2839d7d2 2 index.jpeg
    100644 099df17855c924472b1809d99389d8eee7582d9f 3 index.jpeg
    

    第三栏称为stage number,stage number为2的存储着合并前的版本,stage number为3的存储着合并过来的版本。参见这里

  • 漂亮的git前端工具 – tig

C程序设计语言读书笔记

| Comments

函数与程序结构

为什么要使用函数?

  • 隐藏实现细节
  • 对同一段逻辑,尽可能实现代码复用

函数之间的通信可以通过传递参数,函数返回值和外部变量进行。
关于函数返回值:
默认函数返回值为int类型 如果返回值为一个表达式,那么表达式的值在返回之前将被转换为函数的类型,这个操作可能会丢失信息,所以某些编译器会给出警告信息。

函数声明
如果函数声明中不包含参数,如

double atof();

那么编译器为了兼容老版本的C语言策划你工序,将不会对atof的参数做任何假设,并且会关闭所有的参数检查。所以,如果函数带有参数,则要声明它们;如果没有参数,则使用void进行声明。

变量类型

  • 外部变量

    C语言可以看成是一系列的外部对象构成,这些外部对象包括函数和变量。外部变量可以在全局范围内访问,与内部变量相比,它有更大的作用域和生命周期。任何函数都可以访问一个外部变量,但前提是,这个变量需要通过某种方式声明

  • 自动变量

    在函数内部声明的变量,包括函数参数称为自动变量,它们在函数每次调用时初始化,函数调用完成后自动销毁。

  • 静态变量

    用static声明外部变量和函数,可以将其声明的对象的作用域限定为被编译原文件的剩余部分,通过static声明外部对象,可以达到隐藏外部对象的目的。被static声明的函数的函数名除了对该函数声明的所在的文件可见外,其他文件无法访问。

    static也可用于声明内部变量,它同自动变量一样是局部变量,与自动变量不同的是,不管其所在的函数是否被调用,它一直存在,一直占据存储空间。

  • 寄存器变量 register声明告诉编译器,它所声明的变量在程序中使用的频率较高,暗示将这个变量放在寄存器中,从而执行速度更快,但是,编译器可以选择忽略这个声明。

    register声明只适用于自动变量以及函数的形式参数。

作用域
外部变量和函数的作用域从声明它的地方开始,到其所在的文件末尾结束。

变量的声明与定义
—> 变量的声明只是说明变量的属性和性质,并不分配存储单元;
—> 变量的定义表示要分配存储单元。
变量的声明有两种:

  1. 定义声明(defining declaration)

    在声明一个变量的时候就为这个变量分配存储空间,这构成了对这个变量的定义。

  2. 引用声明(referencing declaration)

    不会分配存储空间,只是告诉编译器要使用这个变量。
    变量只能定义一次,但是可以被声明多次。举例如下:

     int tern = 1; // 定义声明,声明tern的同时定义tern,导致内存空间的分配
     main() {
      extern int tern; // 引用声明,不会分配内存空间
      int tern = 2; // 错误,重复定义
    
      extern int var;
     }
    
     int var = 5;
    

    关键字extern表明这是一个引用声明,只做引用,不做定义。extern主要用在:

    1. 每个需要访问外部变量的函数中,都必须用extern关键字声明相应的外部变量(如上例的tern);
    2. 要在外部变量的定义之前使用该变量(如上例的var变量);
    3. 外部变量的定义在一个文件中,而在其他文件中需要通过extern声明来访问它。

函数的声明与定义
函数的声明(函数原型)与定义是分开的。函数在使用前必须要先声明,如果没有函数原型,那么在函数第一次使用的时候会被隐式声明:该函数的返回值被假定为int型,而对函数参数不做任何假设。

初始化
外部变量和静态变量默认初始化为0,自动变量和寄存器变量的初值没有定义。
外部变量和静态变量的初始化表达式必须是常量表达式,且只初始化一次。自动变量和寄存器变量的初始化表达式可以不是常量表达式,且在每次进入函数或者程序块时都要被初始化一次。

Reference:

Rotating Async Tasks

| Comments

Originally posted by Even Charlton.

A common problem for new Android developers is how to handle screen rotations on the Android platform. Screen rotations cause all sorts of problems if you don’t anticipate them because the Activity gets torn down completely and then rebuilt. If you don’t handle it properly, AsyncTasks (threads) break completely. I’ll show the code first and then talk about why it works. Here’s the general pattern.

The code

public class MyActivity extends Activity {
    private MyTask mTask;
    @Override
    public void onCreate(Bundle savedInstanceState)
        super.onCreate(savedInstanceState)
        mTask = (MyTask) getLastNonConfigurationInstance();
        if(mTask == null) {
            mTask = new MyTask();
        }
        mTask.activity = this;
        if(mTask.getStatus() == AsyncTask.Status.PENDING) {
            mTask.execute();
        }
    }
    @Override
    public Object onRetainNonConfigurationInstance() {
        return mTask;
    }
    private static class MyTask extends AsyncTask<Params, Progress, Result>() {
        public MyActivity activity;
        @Override
        protected void onPreExecute() {
            // ...
        }
        @Override
        protected Result doInBackground(Params... params) {
            // ...
        }
        @Override
        protected void onProgressUpdate(Progress... updates) {
            // ...
        }
        @Override
        protected void onPostExecute(Result result) {
            // ...
        }
    }
}

The explanation

This works because MyTask is a static class—it will survive the class being torn down. All you then do is reattach it to the Activity when you recreate the Activity after the rotation. Note that you don’t just always call mTask.execute() — only call it if it hasn’t been called before.

Of course this might not be perfect for your situation, but I assume that you can make the necessary modifications for your specific case. Feel free to drop me a line if you have any questions.

从Api level 11开始引入了Loader类,该类同AsyncTask一样可以实现异步加载,同时在设计上考虑了configuation改变的场景。

更改git提交历史信息

| Comments

如果你使用git进行版本控制,不知道你有没有遇到这种情况,提交了几个commit后发现你本地git库的author和email信息配置不是你预想的,这时你想要更新已经提交的commit的author和email信息,怎么办?
这是stackoverflow上给出的做法

#!/bin/sh

git filter-branch --env-filter '

an="$GIT_AUTHOR_NAME"
am="$GIT_AUTHOR_EMAIL"
cn="$GIT_COMMITTER_NAME"
cm="$GIT_COMMITTER_EMAIL"

if [ "$GIT_COMMITTER_EMAIL" = "your@email.to.match" ]
then
    cn="Your New Committer Name"
    cm="Your New Committer Email"
fi
if [ "$GIT_AUTHOR_EMAIL" = "your@email.to.match" ]
then
    an="Your New Author Name"
    am="Your New Author Email"
fi

export GIT_AUTHOR_NAME="$an"
export GIT_AUTHOR_EMAIL="$am"
export GIT_COMMITTER_NAME="$cn"
export GIT_COMMITTER_EMAIL="$cm"
'

原文在这里

但是问题是,更新本地的提交历史信息后,想要把更新push到远程分支上去,出现non-fast-forward错误,意思是要推送的提交并非继远程版本库最新提交之后的提交,推送会造成覆盖远程仓库的提交。

$ git push origin master
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'example.git'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes (e.g. 'git pull') before pushing again.  See the
'Note about fast-forwards' section of 'git push --help' for details.

使用—force选项进行强制push即可: git push --force origin master,但是这样会覆盖远程分支上的commit:

-f, --force
    Usually, the command refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it. This flag disables the
    check. This can cause the remote repository to lose commits; use it with care.

另一种方法

使用git rebase -i 到你想更改的commit的前一个commit,选择reword选项,然后依次重新提交,这时提交的author和email将会是你本地配置的author和email,rebase完成后commit的author和email信息就更新了。

Android 权限机制

| Comments

概要

Android基于Linux,Android的权限管理基于Linux的权限框架。此外,Android在此基础上为应用程序提供了permission机制,使得应用程序通过“申请-授予”的机制得以访问系统的敏感资源。权限的授予在应用程序安装过程中就已经确定了,在安装过程中,应用程序的gid,uid,pid已经gids都会被分配。Android不支持在运行时动态进行权限授予。Why?

References

http://en.wikipedia.org/wiki/Group_identifier#Primary_vs._supplementary

Java的四种引用类型

| Comments

Java中有四种引用类型,按其引用强度由强到弱依次为:

Strong reference > Soft reference > Weak reference > Phantom reference

Strong reference

最常用的为strong reference,就是说只要某个对象还被一个强引用引用着,那么GC就不会回收它。使用强引用的一个弊端就是有可能引起内存泄漏。比如有一个hashmap集合,用来存储对象引用,如果你忘了在某个时机把这些元素remove掉,那么这些对象就会在这个hashmap的生命周期内被一直引用而不能被GC回收,更糟糕的是,如果这个对象体积很大,而又如果这个hashmap被声明为static的,那么随着程序的运行,内存总有被撑爆的那一刻。

Soft reference

而soft reference 就不同了,在内存资源极度紧张的情况下,GC会将被Soft reference 引用的对象回收以释放内存空间。这个特性非常适合用来做cache:在内存资源充裕的情况下,它和强引用一样,GC不会回收它,而在内存紧张的情况下,GC实在找不到更多可用的空间的情况下,Soft reference的对象会被回收掉。
以下代码展示了基于Soft reference的缓存类的使用:

/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.providers.contacts;

import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.google.android.collect.Maps;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import java.lang.ref.SoftReference;
import java.util.BitSet;
import java.util.HashMap;


/**
 * Cache for common nicknames.
 */
public class CommonNicknameCache  {

    // We will use this much memory (in bits) to optimize the nickname cluster lookup
    private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF;   // =long[128]
    private BitSet mNicknameBloomFilter;

    private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();

    private final SQLiteDatabase mDb;

    public CommonNicknameCache(SQLiteDatabase db) {
        mDb = db;
    }

    private final static class NicknameLookupPreloadQuery {
        public final static String TABLE = Tables.NICKNAME_LOOKUP;

        public final static String[] COLUMNS = new String[] {
            NicknameLookupColumns.NAME
        };

        public final static int NAME = 0;
    }

    /**
     * Read all known common nicknames from the database and populate a Bloom
     * filter using the corresponding hash codes. The idea is to eliminate most
     * of unnecessary database lookups for nicknames. Given a name, we will take
     * its hash code and see if it is set in the Bloom filter. If not, we will know
     * that the name is not in the database. If it is, we still need to run a
     * query.
     * <p>
     * Given the size of the filter and the expected size of the nickname table,
     * we should expect the combination of the Bloom filter and cache will
     * prevent around 98-99% of unnecessary queries from running.
     */
    private void preloadNicknameBloomFilter() {
        // 这个filter被设计成一个简化的hash表(没有处理hash冲突的情况,实际上也没有必要)
        mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
        Cursor cursor = mDb.query(NicknameLookupPreloadQuery.TABLE,
                NicknameLookupPreloadQuery.COLUMNS,
                null, null, null, null, null);
        try {
            int count = cursor.getCount();
            for (int i = 0; i < count; i++) {
                cursor.moveToNext();
                String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
                int hashCode = normalizedName.hashCode();
                // 将元素put进hash表(有可能冲突),参见HashMap的put实现
                // 和hashcode做与运算的这个值必须是“hash表长度-1”
                mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * Returns nickname cluster IDs or null. Maintains cache.
     */
    protected String[] getCommonNicknameClusters(String normalizedName) {
        if (mNicknameBloomFilter == null) {
            preloadNicknameBloomFilter();
        }

        int hashCode = normalizedName.hashCode();

        // 如果没有找到,说明cache中**一定**不存在这个key所对应的值;
        // 如果找到了,说明cache中**可能**存在这个key对应的值,需要进一步到cache中查找
        if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
            return null;
        }

        SoftReference<String[]> ref;
        String[] clusters = null;

        // 注意:这里需要同步对cache的访问
        synchronized (mNicknameClusterCache) {
            if (mNicknameClusterCache.containsKey(normalizedName)) {
                ref = mNicknameClusterCache.get(normalizedName);
                if (ref == null) {
                    return null;
                }
                clusters = ref.get();
            }
        }

        // 没有命中,这时才需要到DB中加载,并加入cache
        if (clusters == null) {
            clusters = loadNicknameClusters(normalizedName);
            ref = clusters == null ? null : new SoftReference<String[]>(clusters);
            synchronized (mNicknameClusterCache) {
                mNicknameClusterCache.put(normalizedName, ref);
            }
        }
        return clusters;
    }

    private interface NicknameLookupQuery {
        String TABLE = Tables.NICKNAME_LOOKUP;

        String[] COLUMNS = new String[] {
            NicknameLookupColumns.CLUSTER
        };

        int CLUSTER = 0;
    }

    protected String[] loadNicknameClusters(String normalizedName) {
        String[] clusters = null;
        Cursor cursor = mDb.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS,
                NicknameLookupColumns.NAME + "=?", new String[] { normalizedName },
                null, null, null);
        try {
            int count = cursor.getCount();
            if (count > 0) {
                clusters = new String[count];
                for (int i = 0; i < count; i++) {
                    cursor.moveToNext();
                    clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER);
                }
            }
        } finally {
            cursor.close();
        }
        return clusters;
    }
}

这个缓存类实现的实际上是一个二级缓存:

  1. 第一级是一个BitSet,实现为一个hash表,它充当了一个过滤器的作用。第一次加载时会从db中查找所有的数据,并通过hash算法插入到表中的适当位置,这个过程和HashMap的实现是一样的,只不过没有处理hash冲突的情况,当出现hash冲突时,会覆盖表中原有的值。
  2. 第二级是一个HashMap,是真正的cache。其中的元素通过SoftReference引用。

Weak reference

Weak reference和Soft reference类似,区别在于一旦GC启动,被Weak reference引用的对象就会被回收,而不管当前内存资源是否充裕,因此,被Weak reference引用的对象具有更短的生命周期。但是,由于gc是一个优先级比较低的进程,Weak reference的对象不会像你想象中那么快就被回收了。

Phantom reference

Phantom reference 和以上几种引用都不同。它的get()方法永远都会返回null。那么它究竟有什么作用呢?
TODO…

References

NoClassDefFoundError 和 ClassNotFoundException 的区别

| Comments

先看API解释:
For ClassNotFoundException:

Thrown when an application tries to load in a class through its string name using:

    * The forName method in class Class.
    * The findSystemClass method in class ClassLoader.
    * The loadClass method in class ClassLoader.

but no definition for the class with the specified name could be found.

For NoClassDefFoundError:

Thrown if the Java Virtual Machine or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance using the new expression) and no definition of the class could be found.

The searched-for class definition existed when the currently executing class was compiled, but the definition can no longer be found.

简单的说,就是如果使用new SomeClass()的方式创建对象的,而这时ClassLoader装载SomeClass失败(可能因为权限不够,JNI错误,etc),就会抛出NoClassDefFoundError。
而如果使用反射来装载一个对象,但是SomeClass不在CLASSPATH中,这时就会抛出ClassNotFoundException。

Reference:
http://stackoverflow.com/questions/1457863/what-is-the-difference-between-noclassdeffounderror-and-classnotfoundexception

使用Github Pages创建个人博客

| Comments

Github pages 是什么?

Github pages借助 git 工具和Jekyll(一个静态网站生成器),使得你可以将内容push到github,然后通过Jekyll发布成个人blog站点。通过github pages创建个人博客,至少有几个好处:

  • 文章内容使用markdown标记语言书写,你可以使用任何你喜欢的文本编辑器编辑文章。
  • 由于git的存在,你可以轻松的对你的文章进行版本控制。
  • 尤其重要的一点,在天朝,你无需担心哪天你的站点被无端屏蔽,还得爬墙过去抢救数据—因为你本地就有一套完整的站点,而且都是文本格式,非常易于备份。

搭建步骤

创建github帐号

首先,你需要一个github帐号。 登录github后,创建一个USERNAME.github.com的repository,根据向导生成pages。

安装本地环境

你需要在本地搭建运行环境,这样在编写文章的时候你可以在本地进行充分的调整,满意后再push到github。 以下基于Ubuntu 10.04LTS系统安装,需要安装的包包括Ruby,RubyGems,jekyll和rake。

$ sudo apt-get install ruby1.9.1-full rubygems1.9.1

然后通过gem命令安装jekyll

$ sudo gem install jekyll
WARNING:  You don't have /home/calvin/.gem/ruby/1.9.1/bin in your PATH,
      gem executables will not run.
ERROR:  Error installing jekyll:
    liquid requires RubyGems version >= 1.3.7

安装会出错,提示RubyGems版本过低。这时候需要到http://packages.ubuntu.com/maverick/all/rubygems1.8/download 下载离线安装包安装:

$ sudo dpkg -i Downloads/rubygems1.8_1.3.7-2_all.deb

安装完成后查看gem版本:

$ which gem
/usr/bin/gem
$ gem -v
1.3.7

这时候可以继续安装jekyll了:

$ sudo gem install jekyll

安装jekyll后还需要安装rake工具,通过rake工具可以用来创建新的文章和页面,安装主题等等,十分方便。

$ sudo apt-get install rake

通过rake -T命令可以查看所有可用命令:

$ rake -T
(in /home/calvin/development/github/calvinlee.github.com)
rake page           # Create a new page.
rake post           # Begin a new post in ./_posts
rake preview        # Launch preview environment
rake theme:install  # Install theme
rake theme:package  # Package theme
rake theme:switch   # Switch between Jekyll-bootstrap themes.

安装Jekyll Bootstrap

由于jekyll仅仅是一个静态html的生成器,它不包含任何页面的templates,所以如果从头开始构建你的博客站点的话,还将要有很多工作要做。幸好,Jekyll Bootstrap已经提供了一套基本的构架,我们只需要在它的基础上定制,使之符合你口味即可。

$ git clone https://github.com/plusjade/jekyll-bootstrap.git USERNAME.github.com
$ cd USERNAME.github.com
$ git remote set-url origin git@github.com:USERNAME/USERNAME.github.com.git
$ git push origin master

开始个性博客之旅

首先在本地启动jekyll:

$ /var/lib/gems/1.8/gems/jekyll-0.11.2/bin/jekyll --server

然后你可以访问http://localhost:4000%E6%9F%A5%E7%9C%8BJekyll Bootstrap给你提供了什么,之后你就可以开始写你的文章了。

$ rake post title="my-first-post-on-git-pages" date="2012-03-02"

rake工具会为你生成posts/2012-03-02-my-first-post-on-git-pages.md 你可以开始使用markdown标记语言编辑文章了!

发布

发布很简单,直接将文章内容推送到github即可,如果你修改了页面主题或者布局,也一并推送:

$ git add .
$ git commit -m "Add new post"
$ git push origin master

现在你就可以通过http://USERNAME.github.com%E8%AE%BF%E9%97%AE%E4%BD%A0%E7%9A%84%E9%A1%B5%E9%9D%A2%E4%BA%86%E3%80%82

Reference