原文来自Android SDK文档中的docs/resources/articles/backward-compatibility.html

目前有各种Android设备。 这些设备使用不同的Android版本, 有些运行最新的版本, 有些运行较老的版本。 作为开发者, 当考虑如何在应用中保持向后兼容——你是想让你的应用在所有Android设备上运行, 还是只能在最新的版本上运行? 有时有必要既享受新的API带来的便利(如果设备支持的话), 同时继续兼容老的设备。

设置minSdkVersion

如果应用的重要功能使用了新的API(原文if the use of new API is intergral to the application)——比如需要使用Android 1.5(API Level 3)中引入的新的API录制视频, 那么应当在应用的manifest中添加<android:minSdkVersion>,以保证这个应用不会被安装到更老的设备当中。 如果应用依赖于API Level 3中引入的新API, 应当指定minSdkVersion的值为3:

  <manifest>   ...   <uses-sdk android:minSdkVersion="3" />   ...  </manifest>

但是, 如果你只是给应用增加了一个有用但非核心的特性, 比如在可使用实体键盘的情况下提供一个软键盘, 可以使用如下这种方式:既允许在新设备上使用这个特性, 同时不会在老的设备上引起错误。

使用反射

假设想使用一个新的api, 比如android.os.Debug.dumpHprofData(String name)。 Debug类已经在android 1.0中存在, 但是方法是在Android 1.5(API Level 3)中新引入的。 如果你直接调用这个方法, 应用会在Android 1.1或更老的设备上崩溃。

最简单的办法是使用反射来调用这个方法。 这需要进行一次方法查询并将结果保存在一个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()方法, 该方法进行方法查询。 如果查询成功, 就使用跟原始语法相同的方式(argumnets, return value, checked exceptions)来调用这个私有方法。 返回值(如果有的话)和异常以类似于原始方式的形式被解包和返回。 fiddle()方法展示了应用的逻辑是如何来选择调用新的API,或者是根据新的API是否存在来干点别的事。

对每个想调用的新方法, 需要在当前类中添加一个额外的私有Method成员变量, 该成员变量对应的初始化方法, 以及调用包装器(原文: call wrapper)。

如果想调用的方法来自于先前未定义的类(注: 比如Android 1.0中没有, 但是Android 1.1新添加的类), 上述过程变得稍微有些复杂。 另外 , 调用Method.invode()比直接调用会慢很多。 可以使用一个包装类(Wrapper class)来部分减少这两个问题。

使用包装类

思路是添加一个新的包装类, 其作用是包装新添加的API(这些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;   }}

然后为NewClass创建一个包装类

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包含原始类NewClass的各个方法(包括构造方法)的对应的包装方法, 另外还有一个静态初始化块用于检查NewClass类是否存在(注:这里有个小问题, 如果NewClass不存在,WrapNewClass的编译不是通不过吗?答案是, 一般采用新版本的SDK开发, 所以编译不成问题。 但是目标环境可能只支持低版本的SDK, 所以不存在NewClass的定义)。 如果 NewClass不存在, WrapNewClass的初始化过程失败, 注意应保证WrapNewClass(即包装类)不被随意使用。 checkAvailable()方法用于强制执行WrapNewClass的静态初始化块(注:这个初始化块会加载NewClass)。 我们这样使用:

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()方法调用成功, 我们就知道新的class在系统中存在;如果调用失败, 则不存在, 我们需要随之调整预期。 需要注意的是, 如果字节码校验器确信它不想接受这样一个类, 该类的某个成员变量的Class对象根本不存在(注:在 老版本的设备上可能出现这种情况, WrapNewClass的成员变量mInstance的Class对象不存在), 那么 checkAvailable()方法有可能在开始执行之前就失败。 上面代码的这种写法, 可以保证无论异常是来自字节码校验器还是Class.forName()调用, 执行结果都是一致的。

当包装一个添加了新方法的已存在的类, 只需要将新添加的方法的包装方法添加到这个包装类;要使用原本存在的方法, 直接调用即可。 WrapNewClass的静态块会随着每个反射调用增大。(注:对每个可能的新class需要进行检查, 意味着多个Class.forName()调用)。

测试是王道

必须在每个版本的Android平台上测试应用是否如期望的那样能够正常运行。 应用在不同的平台上(注:这里的不同平台应该指的是API发生了变化的平台, 而且应用刚好使用反射方法使用了这些API), 其行为应当不一致。 牢记: 如果没验证过, 它很可能不正确。

可以在老版本的模拟器上测试应用的向后兼容性。 Android SDK可以方便地使用不同的API Level创建"Android虚拟设备"。 创建好AVDs之后, 就可以使用新的和老版本来测试, 还能同时打开不同版本的模拟器来观察应用程序行为。 更多信息可以参考文档中的 Creating and Managing Virtual Devices一章, 或者运行emulator -help -virtual-device来查看帮助信息。

更多相关文章

  1. Android NullPointerException解决方法(空指针异常)
  2. android studio开发安卓应用设置版本号
  3. Android刷Root方法,zergRush,Odin3+CWM(ClockworkMod recovery)
  4. [Android]实现静默安装APK的两种方法
  5. Android中ScrollView隐藏进度条方法
  6. android各个版本的代号
  7. Android Studio Check for Updates检测发现不到新版本
  8. Android百度地图一种简单实现多标注及响应时间的方法
  9. Xposed: 勾住(Hook) Android应用程序对象的方法,实现AOP

随机推荐

  1. 内存优化之其他优化(容器数据遍历方案,arra
  2. Android(安卓)m6.0权限问题调用封装utils
  3. android的apk文件的xml提取
  4. Android底部tab与标题栏相结合
  5. 你还在把Java当成Android官方开发语言吗?K
  6. Android中的帧动画与补间动画的使用
  7. Android的全局通知机制
  8. Android中的消息机制——Looper、Handler
  9. 美国州划分新方式:Android州和iPhone州
  10. 分析方法论探讨