应用程序后向兼容
在全世界,现在人们手里有着各种各样的基于Android的设备。而这些设备中,有很多种Android平台的版本在使用,一些运行着最新版平台,而另一些还在运行着老的版本。作为一名开发人员,你需要考虑你的应用程序是否支持后向兼容——你想你的应用程序能在所有的设备上运行吗,或是只是在最新的平台上运行?在某些情况下,在支持的设备上部署新的API,并支持老的设备是很有用的。
设定minSdkVersion
如果一个新的API的使用对应用程序来说是不可或缺的——也许,你需要使用在Android 1.5(API等级3)中引入的视频录制API——你需要在应用程序的manifest文件中添加<android:minSdkVersion>结点,来确保应用程序不会安装到老的设备上。例如,如果你的应用程序依赖于API 等级3中引入的API,那么,你需要指定“3”作为最低的SDK版本:
<manifest>
...
<uses-sdk android:minSdkVersion="3" />
...
</manifest>
然而,如果你想添加一个有用的但不是必须的特性时,例如在硬件键盘可用的时候弹出一个屏幕键盘,你可以这样书写你的代码:允许你的程序使用新的特征,而在老的设备上不会失败。
使用反射
假设你想使用android.os.Debug.dumpHprofData(String filename)这个新的方法。Debug这个类自从Android 1.0的时候就已经存在了,但这个方法在Android 1.5(API等级3)中才新增的。如果你想直接调用它,那么,在Android 1.1或更早的设备上,你的app将运行失败。
最简单的方式是通过反射的方式来调用这个方法。这需要做一次查找并在Method对象上进行缓存。调用这个方法实质上是在调用Method.invoke,并对结果进行拆箱。参考以下内容:
public class Reflect {
private static Method mDebug_dumpHprofData;
static {
initCompatibility();
};
private static void initCompatibility() {
try {
mDebug_dumpHprofData = Debug.class.getMethod(
"dumpHprofData", new Class[] { String.class } );
/* success, this is a newer device */
} catch (NoSuchMethodException nsme) {
/* failure, must be older device */
}
}
private static void dumpHprofData(String fileName) throws IOException {
try {
mDebug_dumpHprofData.invoke(null, fileName);
} catch (InvocationTargetException ite) {
/* unpack original exception when possible */
Throwable cause = ite.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
} else {
/* unexpected checked exception; wrap and re-throw */
throw new RuntimeException(ite);
}
} catch (IllegalAccessException ie) {
System.err.println("unexpected " + ie);
}
}
public void fiddle() {
if (mDebug_dumpHprofData != null) {
/* feature is supported */
try {
dumpHprofData("/sdcard/dump.hprof");
} catch (IOException ie) {
System.err.println("dump failed!");
}
} else {
/* feature not supported, do something else */
System.out.println("dump not supported");
}
}
}
使用静态初始化方法来调用initCompatibility,进行方法的查找。如果查找成功的话,使用一个私有的方法(与原始的函数签名一致——参数,返回值、异常检查)来替换方法的调用。返回值(如果有的话)和异常都如同原始的方法一样进行返回。fiddle方法演示了程序的选择逻辑,是调用新的API还是在新API无效的情况下作其它的事情。
对于每个你想调用的方法,你可能要添加一个额外的私有Method字段,字段初始化方法,和对调用的包装方法。
如果想调用一个之前未定义的类的方法的话,就比较复杂了。并且,调用Method.invoke()比直接调用这个方法要慢很多。这种情况可以通过一个包装类来缓和一下。
使用包装类
想法是创建一个新的类,来包装新的或已经存在的类暴露出来的所有的新API。包装类中的每个方法只是调用相应的真实方法并返回相同的结果。
如果目标类和方法存在的话,能得到与直接调用相同的行为,并有少量的性能损失。如果目标类或方法不存在的话,包装类的初始化会失败,并且你的应用程序知道必须避免使用这些新的方法。
假设这个类是新增的:
public class NewClass {
private static int mDiv = 1;
private int mMult;
public static void setGlobalDiv(int div) {
mDiv = div;
}
public NewClass(int mult) {
mMult = mult;
}
public int doStuff(int val) {
return (val * mMult) / mDiv;
}
}
我们可能这样创建一个包装类:
class WrapNewClass {
private NewClass mInstance;
/* class initialization fails when this throws an exception */
static {
try {
Class.forName("NewClass");
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/* calling here forces class initialization */
public static void checkAvailable() {}
public static void setGlobalDiv(int div) {
NewClass.setGlobalDiv(div);
}
public WrapNewClass(int mult) {
mInstance = new NewClass(mult);
}
public int doStuff(int val) {
return mInstance.doStuff(val);
}
}
包装类拥有和原始类一模一样的方法和构造函数,加上一个静态的初始化方法和测试方法来检查新类是否存在。如果新类不可获得的话,WrapNewClass的初始化会失败,因此,要确保包装类在这种情况下不要被使用。checkAvailable方法是一种强制类进行初始化的简单方法。我们可以像这样来使用:
public class MyApp {
private static boolean mNewClassAvailable;
/* establish whether the "new" class is available to us */
static {
try {
WrapNewClass.checkAvailable();
mNewClassAvailable = true;
} catch (Throwable t) {
mNewClassAvailable = false;
}
}
public void diddle() {
if (mNewClassAvailable) {
WrapNewClass.setGlobalDiv(4);
WrapNewClass wnc = new WrapNewClass(40);
System.out.println("newer API is available - " + wnc.doStuff(10));
} else {
System.out.println("newer API not available");
}
}
}
如果调用checkAvailable成功,我们知道新的类是系统的一部分。如果它失败了,我们知道新的类不存在,并作相应的调整。应该指出的是,由于字节码校验不支持对一个不存在的类的引用,因此,在调用checkAvailable之前就有可能失败。像实例代码那样构建,结果是一样的,可能字节码校验抛出异常或者Class.forName的调用抛出异常。
当包装一个有新方法的已存类时,你只需要在包装类中添加新的方法。老的方法直接调用。新的方法需要在WrapNewClass的静态初始化方法中作一次反射检查。
测试是关键
你必须测试任何想支持的Android框架版本。一般来说,应用程序在不同的版本上行为不同。记住一条法则:如果你不尝试,它就不能工作。
你可以在老版本平台的模拟器上运行应用程序来测试程序的后向兼容性。由于可以创建不同API等级的“虚拟设备”,因此,你可以很容易地进行测试。一旦你创建了AVD,你就可以在新老版本系统上进行程序测试,也许你还可以一边测试一边观察它们的不同点。更多关于模拟器/虚拟设备的信息,请参考AVD documentation和运行emulator -help-virtual-device
命令。
xirihanlin 译于2010.04.06