(翻译) Backward Compatibility for Applications

原文来自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来查看帮助信息。 

猜你喜欢

转载自410063005.iteye.com/blog/1767986
今日推荐