in 工具(Tool)

Facebook的Dalvik运行期补丁

对于一个功能丰富的Android应用来说,要面对的挑战很多,其中很多问题绝大多数开发者都可能意识不到。Android应用的方法数量限制可能就是一个。

在一些传统的企业应用中,当处理跟业务数据相关的时候,通常不会将数据变量设置为公开访问public,而是会用getter/setter这样的存取方法来封装访问数据模型中的私有变量。在一般的编程实践中,这是一个被推荐和鼓励的做法。但是其副作用是类文件中会产生大量的方法(每一个私有变量都对应着2个方法)。

这个副作用带来的问题是,当应用安装在一些老机型上的时候,可能导致Android的一个Bughttp://code.google.com/p/android/issues/detail?id=22586。原因在于,在应用的安装过程中,会运行一个程序(dexopt)去根据当前机型为应用做一些特定的准备和优化。dexopt中使用了一个固定大小的缓冲区(名为LinearAlloc)来存储应用中方法的信息。最新的几个Android版本中,该缓冲的大小是8MB或16MB。但是Froyo(2.2)和Gingerbread(2.3)只有5MB。因为老版Android的这个限制,当方法数量超过这个缓冲大小的时候,会导致应用崩溃。

解决办法之一是可以利用http://android-developers.blogspot.hk/2011/07/custom-class-loading-in-dalvik.html提到的技术,将dex分成多个dex文件分别加载,首先加载核心dex,其他的模块和扩展功能放在其他的dex文件中。

但是某些时候,如果第二个dex包中的类需要被Android Framework直接访问到的话,上述方案就是不可行的。此时必须要将第二个dex文件注入到Android的系统class loader中。这个一般情况下是无法做到的,但好在Android是开源的系统,可以利用Java的反射去修改Android内部的一些数据结构来解决这个问题。

这个方案在实际运行中,你会发现其实LinearAlloc不仅仅只是在dexopt中存在,而是在每个运行中的Android程序中都有它的身影。dexopt使用LinearAlloc储存dex文件的方法信息的时候,运行期的Android应用使用它访问实际用到的方法。如果在运行期将所有的dex文件都加载到一个进程中的话,实际的方法数量还是会超过限制。应用虽然可以启动但是很快就会崩溃。

看起来似乎要么只能精简应用的功能,要么就只能放弃支持Android部分版本而只支持最新的ICS以上的版本。

再次回头去看Android的源码,找到LinearAlloc缓冲的定义https://github.com/android/platform_dalvik/blob/android-2.3.7_r1/vm/LinearAlloc.h#L33,也许你能意识到:其实只要能将缓冲从5MB增加到8MB就可以安全运转了。

也许,可以使用JNI把当前的缓冲替换成一个更大的缓冲区。这个方案看起来太疯狂了。修改Java的Class Loader的内部是一回事儿,修改运行中的Davlik虚拟机的内部可是另外一回事儿——运行中的代码可能会很危险。但是仔细查看过代码、分析所有使用LinearAlloc的地方后发现,只要在应用启动的时候去做,应该没什么危险。所需要做的就是,找到LinearAlloc对象,锁定,然后替换缓冲。

事实上,只要找到它,后面的事情就很顺利了。在这里https://github.com/android/platform_dalvik/blob/android-2.3.7_r1/vm/Globals.h#L519,DvmGlobals对象中保存了LinearAlloc缓冲区。大概距离对象的开始地址700个字节的位置。从对象的开头开始扫描到这里风险很大,但是幸运的是,可以用距离一步之遥的vmList对象作为起点。这里包含了一个值,可以通过JNI和JavaVM的指针做比较。

最终方案是,找到vmList的值,扫描DvmGlobals对象找到匹配的位置,往后跳几个字节找到LinearAlloc的头,然后替换缓冲区。编写JNI扩展,嵌入到应用中,启动,然后应用应该可以正常运行在Gingerbread上了。

但是又有一个问题是,这个方案在Sumsung Galaxy S II上会失败,这可是最流行的运行Gingerbread的手机。

似乎三星对Android做了一些改动,导致这个方案的失败。其它的厂商可能会做同样的事情。所以这个方案的代码应该得更加健壮才行。

经过观察,在GS II上,LinearAlloc的缓冲区地址距离实际要找的地址只有4个字节。于是调整代码,如果在期望的地址没有找到LinearAlloc,那么就在附近几个字节的范围内查找。做这件事需要获取当前线程的内存映射表,确保没有在附近的查找过程中访问到无效地址(否则会立刻导致应用崩溃)。

(以上就是Facebook的故事,via

理解AnguarJS中的模板编译

在一开始学习AngularJS的过程中,模板的工作原理可能是最令人难解的问题之一。在其他的框架中,一般来说模板就是一个字符串,在这个字符串中使用一个特殊的表达方式可以嵌入一个传入的外部对象的数据,其本质上就是一个字符串的操作:输入字符串,输出字符串,然后通过.innerHTML()把所有搞出来的东西塞到DOM里面去。

很明显AngularJS的模板不是这样做的。它有双向数据绑定,可以完美的在视图中与最新的数据模型同步。它还会自动在ng-click上注册事件监听器,甚至可以将视图中输入控件中的数据同步回数据模型。太神奇了!

这个当然不是什么魔术,事实上将一个模板转换为一个带动态绑定和事件监听功能的动态视图,这中间的过程其实很简单。一旦搞明白了这个过程,对于AngularJS应用的理解也会上升一个台阶。

这里有一个模板

<div ng-app>
    <label>Name:</label>
    <input type="text" ng-model="yourName" placeholder="Enter a name here">
    <h1>Hello {{yourName}}!</h1>
</div>

如你所见,这是个最简单的AngularJS的应用页面。

反向控制(IoC)

第一个需要了解的概念是“反向控制”(IoC)。AngularJS不需要手工启动应用,而是假定你会遵循一定的规则在代码中放一些基本的设定。

例如,AngularJS会假定HTML代码中有一个带有ng-app属性的元素,表示你的应用的“根”节点。它表示AngularJS可以接管这个节点以及之下所有子节点。这一点可以保证AngularJS可以和其他的JavaScript框架共存。当AngularJS启动应用的时候,它会遍历DOM内的节点,并查找这个属性,这个在AngularJS术语中叫“指令”(Directive)。注意这个ng-app指令可以放在任何DOM节点上,包括<html><body>标签。

AngularJS还会假定其他的框架不会接触到它的“根”节点下面的所有内容,否则可能会导致未知行为,或者打破双向数据绑定和事件监听器。

深入模板

<label>Name:</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<h1>Hello {{yourName}}!</h1>

这一部分是模板。“根”节点下面的所有内容都会被视为模板,之后将会被编译。模板与数据模型($scope)和控制器一起构成了动态视图,用户可以在浏览器中看到并与之交互。本例中没有控制器和明确的数据模型,但AngularJS会在后台创建对应的内容。

所有的AngularJS都会有一个$rootScope,它会持有所有子scope的引用,它自身保存的数据也可以被所有的$scope共享。本例中,由于没有明确指定控制器,所以模板会被绑定到这个$rootScope上,并设置一个子$scope

在模板中,给输入控件绑定了一个模型(由ng-model指明的yourName),AngularJS会在这个数据控件上注册一个键盘事件监听器,并且将所有输入到这个控件中的内容自动保存到$rootScope.yourName,但我们并没有也并不需要声明这个变量。scope中的变量会被初始化为undefined,就像正常的JavaScript对象一样。

模板中还用到了$interpolate指令,表示为两对大花括号 {{ yourName }}。这个指令会创建一个$watcher,监听模型的变化,并在发生变化的时候将数据更新到视图上。

接下来看看如何将一个DOMElement转换成动态视图。

编译模板

AngularJS找到ng-app指令之后,它会启动并创建一个新的$rootScope,开始编译“根”节点(带ng-app的那个节点)下面所有的子节点。

接下来发生的事情分为两步,编译和链接(这里借用了编译原理中的术语,但本质上是一样的)。更深的介绍可以参考文档AngularJS documentation on the HTML Compiler

首先,Angular的$compile函数将传入的DOMElement作为输入。这一点与其它的框架很不一样,AngularJS会使用浏览器的API遍历整个DOM,而其它模板只是在做字符串替换。如果需要用字符串作为传入的模板,则先要用$angular.element函数将字符串转为DOMElement。这个函数实际上就是jQuery中的$()函数的。

$compile函数会遍历DOM,并查找“指令”(Directive),将找到的每个“指令“添加到一个列表中,整个DOM遍历完成后,再将列表中的”指令“按照“优先级”排序。之后,执行每个“指令”自己的compile函数,让“指令”有机会去修改DOM。每个指令的compile函数会返回一个“链接”函数,该函数会被拼接成一个完整的链接函数,并被返回。

接下来,AngularJS会执行返回的“链接”函数,对应的scope会被传入到这个执行过程中。这一步中,所有的子“链接”函数都会被执行,并绑定在同一个scope上,或依照“指令”的设定创建一个新的scope。所有的“链接”函数执行完毕后,每个“链接”函数都会返回一组DOMElement,这些DOMElement已经完成数据绑定和事件监听,AngularJS会将它们添加到父节点。

上述过程的伪代码可以表示如下

var $compile = ...; //注入到你的代码
var $rootScope = ...; //注入
var parent = ...; //编译过的模板内容会被添加到该DOMElement下
var template = ...; //我们的模板的DOMElement

var linkFn = $compile(template); //编译模板,返回“链接”函数

var element = linkFn(scope); //“链接”,返回处理好的DOMElement

parent.appendChild(element); //将处理好的DOMElement添加到父节点

总结

基本就是这些了。如你所见,AngularJS完全不同于其他的模板系统,它约定了一些规则,并基于这些规则做了一些假定,之后你就不必将精力耗费在启动代码上,而可以放在真正的应用上。这只是AngularJS的一部分,之后会继续讲讲$watch$digest如何保证视图刷新。

via src

在Android中使用OSGi框架(Knopflerfish)

OSGi是用Java实现的一个模块化服务平台。每个模块被称之为Bundle,可以提供服务,也可以在不重启OSGi框架的情况下被安装或卸载。Knopflerfish是一个完全开源的OSGi R4.2标准的实现。

Android能够无缝的集成现有的Java代码,尽管使用的是与现有java字节码格式不兼容的虚拟机Dalvik,但是它可以轻松的将现有的jar文件和类转换为Android使用的Dalvik字节码格式。由于OSGi框架自身和Bundle都只是普通的jar文件,所以他们都应该可以在Android上运行。事实上,大多数时候是没问题的。

注意:这里只是如何在Android中嵌入OSGi系列文章的第一部分

如果只是想让OSGi框架在Android上跑起来,那么只需要编译Knopflerfish的Android版本,复制到设备上,然后就可以通过命令行启动起来了(见上一篇文章这里)。

现在来看看如何将Knopflerfish和一系列Bundle嵌入到Android应用中,并且从应用中启动和管理OSGi框架和Bundle。

通过代码启动OSGi大概需要下面这几个步骤:

  1. 创建framework实例(通过framework factory)
  2. 初始化framework
  3. 设置initlevel,并启动/安装 bunldes
  4. 为所有的initlevel重复前述步骤
  5. 设置startlevel
  6. 启动framework

嵌入Framework

现在创建一个Android应用,包含一个Actviity。然后在app中引入framework.jar,这样就可以通过一个FrameworkFactory创建OSGi的framework实例了。

import org.knopflerfish.framework.FrameworkFactoryImpl;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
...
private Framework mFramework;
...
Dictionary fwprops = new Hashtable();
// add any framework properties to fwprops
FrameworkFactory ff = new FrameworkFactoryImpl();
mFramework = ff.newFramework(fwprops);
try {
    mFramework.init();
} catch (BundleException be) {
    // framework initialization failed
}

引入的jar文件不需要dex化,后面build的时候会自动完成这一步的。

Bundle文件dex化

现在bundle的jar文件可以被添加到应用中了,可以作为raw资源放在res/raw下面,也可以放在assets/bunldes。后面这种方式有一个优势:不需要被重命名,而且res资源的名字数量是有限的。

Bundle的jar文件需要被转换成dex格式,下面这个简单的脚本可以完成这件事:

dexify() {
    for f in $*; do
        tmpdir="`mktemp -d`"
        tmpfile="${tmpdir}/classes.dex"
        dx --dex --output=${tmpfile} ${f}
        aapt add ${f} ${tmpfile}
        rm -f ${tmpfile}
        rmdir ${tmpdir}
    done
}

然后就可以通过命令dexify assets/bundles/*将bundles转换为dex文件。如果是按照Knopflerfish的教程编译的Knopflerfish,那么不需要将这些bundle的jar文件dex化,但是必须从knopflerfish的framework.jar文件中去掉classes.dex。

安装、启动Bundles

下面这段代码可以帮助启动bundle,并设置initlevel/startlevel。

private void startBundle(String bundle) {
    Log.d(TAG, "starting bundle " + bundle);
    InputStream bs;
    try {
        bs = getAssets().open("bundles/" + bundle);
    } catch (IOException e) {
        Log.e(TAG, e.toString());
        return;
    }

    long bid = -1;
    Bundle[] bl = mFramework.getBundleContext().getBundles();
    for (int i = 0; bl != null && i < bl.length; i++) {
        if (bundle.equals(bl[i].getLocation())) {
            bid = bl[i].getBundleId();
        }
    }

    Bundle b = mFramework.getBundleContext().getBundle(bid);
    if (b == null) {
        Log.e(TAG, "can't start bundle " + bundle);
        return;
    }

    try {
        b.start(Bundle.START_ACTIVATION_POLICY);
        Log.d(TAG, "bundle " + b.getSymbolicName() + "/" + b.getBundleId() + "/"
                + b + " started");
    } catch (BundleException be) {
        Log.e(TAG, be.toString());
    }

    try {
        bs.close();
    } catch (IOException e) {
        Log.e(TAG, e.toString());
    }
}

private void installBundle(String bundle) {
    Log.d(TAG, "installing bundle " + bundle);
    InputStream bs;
    try {
        bs = getAssets().open("bundles/" + bundle);
    } catch (IOException e) {
        Log.e(TAG, e.toString());
        return;
    }

    try {
        mFramework.getBundleContext().installBundle(bundle, bs);
        Log.d(TAG, "bundle " + bundle + " installed");
    } catch (BundleException be) {
        Log.e(TAG, be.toString());
    }

    try {
        bs.close();
    } catch (IOException e) {
        Log.e(TAG, e.toString());
    }
}

private void setStartLevel(int startLevel) {
    ServiceReference sr = mFramework.getBundleContext()
        .getServiceReference(StartLevel.class.getName());
    if (sr != null) {
        StartLevel ss =
            (StartLevel)mFramework.getBundleContext().getService(sr);
        ss.setStartLevel(startLevel);
        mFramework.getBundleContext().ungetService(sr);
    } else {
        Log.e(TAG, "No start level service " + startLevel);
    }
}

private void setInitlevel(int level) {
    ServiceReference sr = mFramework.getBundleContext()
        .getServiceReference(StartLevel.class.getName());
    if (sr != null) {
        StartLevel ss =
            (StartLevel)mFramework.getBundleContext().getService(sr);
        ss.setInitialBundleStartLevel(level);
        mFramework.getBundleContext().ungetService(sr);
        Log.d(TAG, "initlevel " + level + " set");
    } else {
        Log.e(TAG, "No start level service " + level);
    }
}

现在可以安装并启动bundle了

setInitlevel(1);
installBundle("event_all-3.0.4.jar");
startBundle("event_all-3.0.4.jar");
// install/start other bundles...

setStartLevel(10);

try {
    mFramework.start();
} catch (BundleException be) {
    Log.e(TAG, be.toString());
    // framework start failed
}

Log.d(TAG, "OSGi framework running, state: " + mFramework.getState());

问题

如果你按照上文所述一步步做下来了,你可能会发现还是没法跑起来。由于framework的classloader是在运行期加载的bundle文件,Dalvik虚拟机会试图将优化过的dex类文件放到一个系统目录下面/data/dalvik-cache,但是没有root权限的普通应用程序是不能写入那儿的。

下回将如何解决这个问题。

via source

在Android中使用OSGi框架(Apache Felix)

本文描述了如何在Android中使用Apache Felix

Dalvik VM

Android允许开发者使用Java开发应用,但出于某些原因,代码实际是运行在名为Dalvik的一个针对移动设备平台的虚拟机上,而不是标准的Java虚拟机。Dalvik并不使用标准的Java字节码格式,而是使用Android SDK中的一个工具dx将由Java编译出来的类文件转换为另外一种类文件格式(.dex格式)。这个转换是在编译期完成的。

准备Bundles

虽然Felix从1.0.3开始内置了Android的支持,但是想要成功的让它跑起来还是需要费点力气。我们仍然需要安装Android SDK,并且PATH环境变量中包含Android SDK的工具目录<android_SDK_HOME>/tools

第一步: 每一个用到的Jar文件,无论是Felix库还是你自己写的Bundle,都需要包含对应的DEX。也就说,需要为jar文件创建对应的dex文件:

dx --dex --output=classes.dex JAR_file.jar

然后将这个dex文件加入到jar文件中:

aapt add JAR_file.jar classes.dex

第二步: 将处理过的jar文件传到模拟器(或真机)中:

adb push JAR_file.jar path_emulator/JAR_file.jar

第三步: 以演示代码为例,准备Felix的jar文件和Bundle的jar文件:

目录结构

osgi-android: /
\- bin
\- bundle
\- conf
\- felix.sh

准备Felix jar文件

export PATH=<path-to-android>/tools:$PATH
cd bin
dx --dex --output=classes.dex felix.jar
aapt add felix.jar classes.dex

准备bundle的jar文件

cd bundle
dx --dex --output=classes.dex     org.apache.felix.shell-1.0.0.jar
aapt add org.apache.felix.shell-1.0.0.jar classes.dex
dx --dex --output=classes.dex org.apache.felix.shell.tui-1.0.0.jar
aapt add org.apache.felix.shell.tui-1.0.0.jar classes.dex
dx --dex --output=classes.dex EnglishDictionary.jar
aapt add EnglishDictionary.jar classes.dex
dx --dex --output=classes.dex FrenchDictionary.jar
aapt add FrenchDictionary.jar classes.dex
dx --dex --output=classes.dex SpellChecker.jar
aapt add SpellChecker.jar classes.dex

复制到模拟器中

cd osgi-android
find * -type f -exec adb push {} /data/felix/{} \;

启动Felix

完成上面的步骤之后,现在可以准备在Android上启动Felix和Bundle了

adb shell
cd /data/felix
sh felix.sh

felix.sh是一个shel脚本,用于启动Felix main class。

/system/bin/dalvikvm -Xbootclasspath:/system/framework/core.jar \

-classpath bin/felix.jar org.apache.felix.main.Main

如果一切顺利,现在你应该能看到Felix的命令行shell了。输入help可以看到命令说明。

现在可以安装EnglishDictionary,FrenchDictionary和SpellChecker来试试看Felix是否工作正常。这里有几个Apache Felix的示例:Apache Felix 教程例子2Apache Felix 教程例子2bApache Felix 教程例子5

  • EnglishDictionary - 提供一个字典服务,支持下面几个词"welcome", "to", "the", "osgi", "tutorial"
  • FrenchDictionary - 提供一个字典服务,支持下面几个词"bienvenue", "au", "tutoriel", "osgi"
  • SpellChecker - 提供一个拼写检查服务,可以检查第一个英文此单的几个单词

在Felix Shell中启动Bundle

start file:bundle/EnglishDictionary.jar
start file:bundle/FrenchDictionary.jar
start file:bundle/SpellChecker.jar

嵌入Felix

Apache Felix也可以被集成到Android的应用中。只需要在Activity的onCreate中嵌入Felix,然后用上面的办法启动bundle即可。

下载

上面的演示代码在此下载

via source

3rd-party apt-key list for Ubuntu

Google Chrome

wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
sudo apt-get update
sudo apt-get install google-chrome-stable

JDK 1.7

echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu precise main" | tee -a /etc/apt/sources.list
echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu precise main" | tee -a /etc/apt/sources.list
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys EEA14886
apt-get update
apt-get install oracle-java7-installer

NodeJS

sudo add-apt-repository ppa:chris-lea/node.js 

MongoDB

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list

Failed to clone a large git repository: The remote end hung up unexpectedly

When git clone a large repository sometimes it give you the error message like this:

$ git clone ssh://xxx@xxx:29418/xxx
Cloning into 'xxx'...
remote: Counting objects: 356213, done
remote: Finding sources: 100% (356213/356213)
Corrupted MAC on input. (277847/356213), 123.59 MiB | 3.86 MiB/s
Finished discarding for xxx
fatal: The remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed

If you got this error message, you might want to try run the following command to fix it

git config --global http.postBuffer 524288000
git config --global --add core.compression -1

genymotion Qt error in Ubuntu

When I was trying to install Genymotion on Ubuntu 14.04, I got this error

Installing log handler
Logging activities to file: /home/****
Aborted (core dumped)

the log file /home/xx/.Genymotion/genymotion.log has the following message:

... [Genymotion] [Fatal] Cannot mix incompatible Qt library (version 0x40806) with this library (version 0x40804)

The reason of this error is that Genymotion has its own Qt library which is not compatable with the system Qt library.

To fix this error, we need to let genymotion use system's Qt library. The following commands can fix this issue

sudo apt-get install libxi-dev libxmu-dev
# go to your genymotion directory
mkdir QtLibs && mv *Qt*.so* QtLibs