在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版本,复制到设备上,然后就可以通过命令行启动起来了(见[上一篇文章](http://log4think.com/use-apache-felix-in-android/)和[这里](http://log4think.com/use-apache-felix-in-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的[教程](http://www.knopflerfish.org/releases/3.2.0/docs/android_dalvik_tutorial.html)编译的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](http://nilvec.com/embedding-osgi-into-an-android-application-part-1.html)

为Java运行环境导入根证书解决Eclipse的TFS插件的"PKIX path building failed"错误

最近在项目中必须要使用微软的TFS作为项目管理工具以及版本控制,而开发的IDE使用的是Eclipse。好在TFS有一个Eclipse的插件能够在跨平台的环境下工作。不过这个插件的11.0版本中,连结服务器的时候如果使用HTTPS连结,可能会有一个证书的认证错误问题,

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
...
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
...

这个问题的是由于Java自带的根证书库中不包含HTTPS服务器上的根证书,因此无法得到认证。解决办法是将服务器的根证书导入到Java运行环境中的根证书库中。假设我的证书文件是zero.cer,首先确保命令行下所引用的java是下文命令中所指向```JAVA_HOME/jre/bin```的是一个JRE,如果不是则需要修改JAVA_HOME的位置。 keytool -import -noprompt -trustcacerts -alias zero -file zero.cer -keystore ~/mykeystore

这个命令会新建一个keystore文件,命令运行后会要求输入密码,这个密码是新建的keystore文件的访问密码。

For Linux:

sudo keytool -importkeystore -srckeystore ~/mykeystore -destkeystore JAVA_HOME/jre/lib/security/cacerts

For Mac:

$ sudo keytool -importkeystore -srckeystore ~/mykeystore -destkeystore /Library/Java/Home/lib/security/cacerts

第二个命令将之前建好的keystore中的证书导入jre自带的keystore文件中。过程中会要求输入目标keystore(也就是jre自带的keystore)文件的密码,这个密码默认是 changeit (linux和mac下),或者是 changeme 。

之后,重启Eclipse,再次连结TFS服务器,即可成功通过服务器验证。此外,可以下载一个小工具SSLPoke验证证书是否导入正常,下载之后在文件所在目录下,命令行运行

$ java SSLPoke tfs.yourserver.com 4343
Successfully connected

如果显示成功则说明证书导入的没有问题。

ant 中通过重新定义 project.all.jars.path 在 classpath 中引入外部 jar 文件

在Android开发中,除了通常在Eclipse中的编译方法之外,有的时候为了进行持续集成,可能还需要用ant进行自动化编译。Android SDK本身已经提供了默认的ant编译脚本,就在每个工程下的build.xml中,其中引用了SDK的编译脚本${sdk_dir}/tools/ant/build.xml 。 通常情况下,在工程根目录下直接执行 ant debug 即可进行一次正常的build。默认的classpath会包括libs目录下的所有jar文件。但是如果工程中使用了USER LIBRARY,或者引用了外部的jar文件,那么在编译中就可能会遇到问题,因为这些jar文件不会被自动包含在classpath中,这时就要扩展ant的path变量,把自己的jar文件加入到classpath中。

通过察看sdk提供的build.xml编译脚本,可以发现javac使用的classpath定义如下:

<path id="project.javac.classpath">
    <path refid="project.all.jars.path"></path>
    <path refid="tested.project.classpath"></path>
</path>


<javac encoding="{java.encoding}"         source="{java.source}" target="{java.target}"         debug="true" extdirs="" includeantruntime="false"         destdir="{out.classes.absolute.dir}"
        bootclasspathref="project.target.class.path"
        verbose="{verbose}"         classpathref="project.javac.classpath"         fork="{need.javac.fork}">
    <src path="{source.absolute.dir}">     

其中 project.all.jars.path 包含了所有的jar文件,我们可以通过在工程目录下的buildxml中重新定义这个变量来引入其他的jar文件。例如在我的工程中,引用了ormlite这个ORM库,为了能够在开发中使用“attach source”察看源码,该jar文件不能放在libs目录中,因为Eclipse不允许对libs目录中的jar文件“attach source”。因此我将此文件放到了libs/ormlite目录中,为了能够将这两个jar文件加入到classpath中,就要重新定义 project.all.jars.path 这个元素。

基本思路是,重新定义-pre-compile这个target,在其中重新定义 project.all.jars.path 的值。

<target name="-pre-compile">
    <echo message="JARPATH={toString:project.all.jars.path}">      {toString:project.all.jars.path}">         {ormlite.dir}/ormlite-core-4.41.jar">