1. 程式人生 > >Android 應用程式向低版本相容的問題

Android 應用程式向低版本相容的問題

在全世界,現在人們手裡有著各種各樣的基於Android的裝置。而這些裝置中,有很多種Android平臺的版本在使用,一些執行著最新版平臺,而另一些還在執行著老的版本。作為一名開發人員,你需要考慮你的應用程式是否支援後向相容——你想你的應用程式能在所有的裝置上執行嗎,或是隻是在最新的平臺上執行?在某些情況下,在支援的裝置上部署新的API,並支援老的裝置是很有用的。

可以設定minSdkVersion:
  1. <manifest>
  2. <uses-sdk android:minSdkVersion="3" />
  3. </manifest>
複製程式碼 然而,如果你想新增一個有用的但不是必須的特性時,例如在硬體鍵盤可用的時候彈出一個螢幕鍵盤,你可以這樣書寫你的程式碼:允許你的程式使用新的特徵,而在老的裝置上不會失敗

使用反射

假設你想使用android.os.Debug.dumpHprofData(String filename)這個新的方法。Debug這個類自從Android 1.0的時候就已經存在了,但這個方法在Android 1.5(API等級3)中才新增的。如果你想直接呼叫它,那麼,在Android 1.1或更早的裝置上,你的app將執行失敗。

最簡單的方式是通過反射的方式來呼叫這個方法。這需要做一次查詢並在Method物件上進行快取。呼叫這個方法實質上是在呼叫Method.invoke,並對結果進行拆箱。參考以下內容:

  1. public class Reflect {
  2. private static Method mDebug_dumpHprofData;
  3. static {
  4. initCompatibility();
  5. };
  6. private static void initCompatibility() {
  7. try {
  8. mDebug_dumpHprofData = Debug.class.getMethod(
  9. "dumpHprofData", new Class[] { String.class } );
  10. /* success, this is a newer device */
  11. } catch (NoSuchMethodException nsme) {
  12. /* failure, must be older device */
  13. }
  14. }
  15. private static void dumpHprofData(String fileName) throws IOException {
  16. try {
  17. mDebug_dumpHprofData.invoke(null, fileName);
  18. } catch (InvocationTargetException ite) {
  19. /* unpack original exception when possible */
  20. Throwable cause = ite.getCause();
  21. if (cause instanceof IOException) {
  22. throw (IOException) cause;
  23. } else if (cause instanceof RuntimeException) {
  24. throw (RuntimeException) cause;
  25. } else if (cause instanceof Error) {
  26. throw (Error) cause;
  27. } else {
  28. /* unexpected checked exception; wrap and re-throw */
  29. throw new RuntimeException(ite);
  30. }
  31. } catch (IllegalAccessException ie) {
  32. System.err.println("unexpected " + ie);
  33. }
  34. }
  35. public void fiddle() {
  36. if (mDebug_dumpHprofData != null) {
  37. /* feature is supported */
  38. try {
  39. dumpHprofData("/sdcard/dump.hprof");
  40. } catch (IOException ie) {
  41. System.err.println("dump failed!");
  42. }
  43. } else {
  44. /* feature not supported, do something else */
  45. System.out.println("dump not supported");
  46. }
  47. }
  48. }
複製程式碼

使用靜態初始化方法來呼叫initCompatibility,進行方法的查詢。如果查詢成功的話,使用一個私有的方法(與原始的函式簽名一致——引數,返回值、異常檢查)來替換方法的呼叫。返回值(如果有的話)和異常都如同原始的方法一樣進行返回。fiddle方法演示了程式的選擇邏輯,是呼叫新的API還是在新API無效的情況下作其它的事情。

對於每個你想呼叫的方法,你可能要新增一個額外的私有Method欄位,欄位初始化方法,和對呼叫的包裝方法。

如果想呼叫一個之前未定義的類的方法的話,就比較複雜了。並且,呼叫Method.invoke()比直接呼叫這個方法要慢很多。這種情況可以通過一個包裝類來緩和一下。

使用包裝類

想法是建立一個新的類,來包裝新的或已經存在的類暴露出來的所有的新API。包裝類中的每個方法只是呼叫相應的真實方法並返回相同的結果。

如果目標類和方法存在的話,能得到與直接呼叫相同的行為,並有少量的效能損失。如果目標類或方法不存在的話,包裝類的初始化會失敗,並且你的應用程式知道必須避免使用這些新的方法。

假設這個類是新增的:

  1. public class NewClass {
  2. private static int mDiv = 1;
  3. private int mMult;
  4. public static void setGlobalDiv(int div) {
  5. mDiv = div;
  6. }
  7. public NewClass(int mult) {
  8. mMult = mult;
  9. }
  10. public int doStuff(int val) {
  11. return (val * mMult) / mDiv;
  12. }
  13. }
複製程式碼


我們可能這樣建立一個包裝類:

  1. class WrapNewClass {
  2. private NewClass mInstance;
  3. /* class initialization fails when this throws an exception */
  4. static {
  5. try {
  6. Class.forName("NewClass");
  7. } catch (Exception ex) {
  8. throw new RuntimeException(ex);
  9. }
  10. }
  11. /* calling here forces class initialization */
  12. public static void checkAvailable() {}
  13. public static void setGlobalDiv(int div) {
  14. NewClass.setGlobalDiv(div);
  15. }
  16. public WrapNewClass(int mult) {
  17. mInstance = new NewClass(mult);
  18. }
  19. public int doStuff(int val) {
  20. return mInstance.doStuff(val);
  21. }
  22. }
複製程式碼


包裝類擁有和原始類一模一樣的方法和建構函式,加上一個靜態的初始化方法和測試方法來檢查新類是否存在。如果新類不可獲得的話,WrapNewClass的初始化會失敗,因此,要確保包裝類在這種情況下不要被使用。checkAvailable方法是一種強制類進行初始化的簡單方法。我們可以像這樣來使用:

  1. public class MyApp {
  2. private static boolean mNewClassAvailable;
  3. /* establish whether the "new" class is available to us */
  4. static {
  5. try {
  6. WrapNewClass.checkAvailable();
  7. mNewClassAvailable = true;
  8. } catch (Throwable t) {
  9. mNewClassAvailable = false;
  10. }
  11. }
  12. public void diddle() {
  13. if (mNewClassAvailable) {
  14. WrapNewClass.setGlobalDiv(4);
  15. WrapNewClass wnc = new WrapNewClass(40);
  16. System.out.println("newer API is available - " + wnc.doStuff(10));
  17. } else {
  18. System.out.println("newer API not available");
  19. }
  20. }
  21. }
複製程式碼

如果呼叫checkAvailable成功,我們知道新的類是系統的一部分。如果它失敗了,我們知道新的類不存在,並作相應的調整。應該指出的是,由於位元組碼校驗不支援對一個不存在的類的引用,因此,在呼叫checkAvailable之前就有可能失敗。像例項程式碼那樣構建,結果是一樣的,可能位元組碼校驗丟擲異常或者Class.forName的呼叫丟擲異常。

當包裝一個有新方法的已存類時,你只需要在包裝類中新增新的方法。老的方法直接呼叫。新的方法需要在WrapNewClass的靜態初始化方法中作一次反射檢查。

測試是關鍵

你必須測試任何想支援的Android框架版本。一般來說,應用程式在不同的版本上行為不同。記住一條法則:如果你不嘗試,它就不能工作。

你可以在老版本平臺的模擬器上執行應用程式來測試程式的後向相容性。由於可以建立不同API等級的“虛擬裝置”,因此,你可以很容易地進行測試。一旦你建立了AVD,你就可以在新老版本系統上進行程式測試,也許你還可以一邊測試一邊觀察它們的不同點。