天天看點

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

美團•大衆點評是中國最大的O2O交易平台,目前已擁有近6億使用者,合作各類商戶達432萬,訂單峰值突破1150萬單。美團App是平台主要的入口之一,O2O交易場景的複雜性決定了App穩定性要達到近乎苛刻的要求。使用者到店消費買優惠券時死活下不了單,定外賣一個明顯可用的紅包怎麼點也選不中,上了一個新活動使用者一點就Crash……過去發生過的這些畫面太美不敢想象。用戶端相對Web版最大的短闆就是有發版的概念,對線上事故很難有即時生效的解決方式,每次發版都如臨深淵如履薄冰,畢竟就算再完善的開發測試流程也無法保證不會将Bug帶到線上。

從去年開始,Android平台出現了一些優秀的熱更新方案,主要可以分為兩類:一類是基于multidex的熱更新架構,包括Nuwa、Tinker等;另一類就是native hook方案,如阿裡開源的Andfix和Dexposed。這樣用戶端也有了實時修複線上問題的可能。但經過調研之後,我們發現上述方案或多或少都有一些問題,基于native hook的方案:需要針對dalvik虛拟機和art虛拟機做适配,需要考慮指令集的相容問題,需要native代碼支援,相容性上會有一定的影響;基于Multidex的方案,需要反射更改DexElements,改變Dex的加載順序,這使得patch需要在下次啟動時才能生效,實時性就受到了影響,同時這種方案在android N [speed-profile]編譯模式下可能會有問題,可以參考Android N混合編譯與對熱更新檔影響解析。考慮到美團Android使用者機型分布的碎片化,很難有一個方案能覆寫所有機型。

去年底的Android Dev Summit上,Google高調釋出了Android Studio 2.0,其中最重要的新特性Instant Run,實作了對代碼修改的實時生效(熱插拔)。我們在了解Instant Run原理之後,實作了一個相容性更強的熱更新方案,這就是産品化的hotpatch架構--Robust。

原理

Robust插件對每個産品代碼的每個函數都在編譯打包階段自動的插入了一段代碼,插入過程對業務開發是完全透明。如State.java的getIndex函數:

public long getIndex() {
        return 100;
    }
           

被處理成如下的實作:

public static ChangeQuickRedirect changeQuickRedirect;
    public long getIndex() {
        if(changeQuickRedirect != null) {
            //PatchProxy中封裝了擷取目前className和methodName的邏輯,并在其内部最終調用了changeQuickRedirect的對應函數
            if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
                return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
            }
        }
        return 100L;
    }
           

可以看到Robust為每個class增加了個類型為ChangeQuickRedirect的靜态成員,而在每個方法前都插入了使用changeQuickRedirect相關的邏輯,當 changeQuickRedirect不為null時,可能會執行到accessDispatch進而替換掉之前老的邏輯,達到fix的目的。

如果需将getIndex函數的傳回值改為return 106,那麼對應生成的patch,主要包含兩個class:PatchesInfoImpl.java和StatePatch.java。

PatchesInfoImpl.java:

public class PatchesInfoImpl implements PatchesInfo {
    public List<PatchedClassInfo> getPatchedClassesInfo() {
        List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
        PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
        patchedClassesInfos.add(patchedClass);
        return patchedClassesInfos;
    }
}
           

StatePatch.java:

public class StatePatch implements ChangeQuickRedirect {
    @Override
    public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
            return 106;
        }
        return null;
    }

    @Override
    public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
            return true;
        }
        return false;
    }
}
           

用戶端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex後,用DexClassLoader加載patch.dex,反射拿到PatchesInfoImpl.java這個class。拿到後,建立這個class的一個對象。然後通過這個對象的getPatchedClassesInfo函數,知道需要patch的class為com.meituan.sample.d(com.meituan.sample.State混淆後的名字),再反射得到目前運作環境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段指派為用patch.dex中的StatePatch.java這個class new出來的對象。這就是打patch的主要過程。通過原理分析,其實Robust隻是在正常的使用DexClassLoader,是以可以說這套架構是沒有相容性問題的。

大體流程如下:

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

插件的問題

OK,到這裡Robust原理就介紹完了。很簡單是不是?而且sample這個例子中也驗證成功了。難道一切這麼順利?其實作實并不是這樣,我們将這套實作用到美團的主App時,問題出現了:

居然不能打出包來了!從原理上分析,除了引入的patch過程aar外,我們這套實作是不會增加别的方法的,而且引入的那個aar的方法才100個左右,怎麼會造成美團的mainDex超過65536呢?進一步分析,我們一共處理7萬多個函數,導緻最後方法數總共增加7661個。這是為什麼呢?

看下patch前後的dex對比:

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

針對com.meituan.android.order.adapter.OrderCenterListAdapter.java分析一下,發現進行hotpatch之後增加了如下6個方法:

public boolean isEditMode() {
        return isEditMode;
    }
private int incrementDelCount() {
        return delCount.incrementAndGet();
    }
private boolean isNeedDisplayRemainingTime(OrderData orderData) {
        return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) > 0;
    }
private boolean isNeedDisplayUnclickableButton(OrderData orderData) {
        return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) <= 0;
    }
private boolean isNeedDisplayExpiring(boolean expiring) {
        return expiring && isNeedDisplayExpiring;
    }
private View getViewByTemplate(int template, View convertView, ViewGroup parent) {
        View view = null;
        switch (template) {
            case TEMPLATE_DEFALUT:
            default:
                view = mInflater.inflate(R.layout.order_center_list_item, null);
        }
        return view;
    }
           

但是這些多出來的函數其實就在原來的産品代碼中,為什麼沒有Robust的情況下不見了,而使用了插件後又出現在最終的class中了呢?隻有一個可能,就是ProGuard的内聯受到了影響。使用了Robust插件後,原來能被ProGuard内聯的函數不能被内聯了。看了下ProGuard的Optimizer.java的相關片段:

if (methodInliningUnique) {
    // Inline methods that are only invoked once.
    programClassPool.classesAccept(
        new AllMethodVisitor(
        new AllAttributeVisitor(
        new MethodInliner(configuration.microEdition,
                          configuration.allowAccessModification,
                          true,
                          methodInliningUniqueCounter))));
}
if (methodInliningShort) {
    // Inline short methods.
    programClassPool.classesAccept(
        new AllMethodVisitor(
        new AllAttributeVisitor(
        new MethodInliner(configuration.microEdition,
                          configuration.allowAccessModification,
                          false,
                          methodInliningShortCounter))));
}
           

通過注釋可以看出,如果隻被調用一次或者足夠小的函數,都可能被内聯。深入分析代碼,我們發現确實如此,隻被調用了一次的私有函數、隻有一行函數體的函數(比如get、set函數等)都極可能内聯。前面com.meituan.android.order.adapter.OrderCenterListAdapter.java多出的那6個函數也證明了這一點。知道原因了就能有解決問題的思路。

其實仔細思考下,那些可能被内聯的隻有一行函數體的函數,真的有被插件處理的必要嗎?别說一行代碼的函數出問題的可能性小,就算出問題了也可以通過patch内聯它的那個函數來解決問題,或者patch這一行代碼調用的那個函數。隻調用了一次的函數其實是一樣的。是以通過分析,這樣的函數其實是可以不被插件處理的。那麼有了這個認識,我們對插件做了處理函數的判斷,跳過被ProGuard内聯可能性比較大的函數。重新在團購試了一次,這次apk順利的打包出來了。通過對打出來apk中的dex做分析,發現優化後的插件還是影響了内聯效果,不過隻導緻方法數增加了不到1000個,是以算是臨時簡單的解決了這個問題。

影響

原理上,Robust是為每個函數都插入了一段邏輯,為每個class插入了ChangeQuickRedirect的字段,是以最終肯定會增加apk的體積。以美團主App為例,平均一個函數會比原來增加17.47個位元組,整個App中我們一共處理了6萬多個函數,導緻包大小由原來的19.71M增加到了20.73M。有些class沒有必要添加ChangeQuickRedirect字段,以後可以通過将這些class過濾掉的方式來做優化。

Robust在每個方法前都加上了額外的邏輯,那對性能上有什麼影響呢?

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

從圖中可以看到,對一個隻有記憶體運算的函數,處理前後分别執行10萬次的時間增加了128ms。這是在華為4A上的測試結果。

對啟動速度上的影響:

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

在同一個機器上的結果,處理前後的啟動時間相差了5ms。

更新檔的問題

再來看看更新檔本身。要制作出更新檔,我們可能會面臨如下兩個問題:

1. 如何解決混淆問題?
2. 被補的函數中使用了super相關的調用怎麼辦?
           

其實混淆的問題比較好處理。先針對混淆前的代碼生成patch.class,然後利用生成release包時對應的mapping檔案中的class的映射關系,對patch.class做字元串上的處理,讓它使用線上運作環境中混淆的class。

被補的函數中使用了super相關的調用怎麼辦?比如某個Activity的onCreate方法中需要調用super.onCreate,而現在這個bad.Class的badMethod就是這個Activity的onCreate方法,那麼在patched.class的patchedMethod中如何通過這個Activity的對象,調用它父類的onCreate方法呢?通過分析Instant Run對這個問題的處理,發現它是在每個class中都添加了一個代理函數,專門來處理super的問題的。為每個class都增加一個函數無疑會增加總的方法數,這樣做肯定會遇到65536這個問題。是以直接使用Instant Run的做法顯然是不可取的。

在Java中super是個關鍵字,也無法通過别的對象來通路到。看來,想直接在patched.java代碼中通過Activity的對象調用到它父類的onCreate方法有點不太可能了。不過通過對class檔案做分析,發現普通的函數調用是使用JVM指令集的invokevirtual指令,而super.onCreate的調用使用的是invokesuper指令。那是不是将class檔案中這個調用的指令改為invokesuper就好了?看如下的例子:

産品代碼SuperClass.java:

public class SuperClass {
    String uuid;
    public void setUuid(String id) {
        uuid = id;
    }
    public void thisIsSuper() {
        Log.d("SuperClass", "thisIsSuper "+uuid);
    }
}
           

産品代碼TestSuperClass.java:

public class TestSuperClass extends SuperClass{
    String subUuid;
    public void setSubUuid(String id) {
        subUuid = id;
    }

    @Override
    public void thisIsSuper() {
        Log.d("TestSuperClass", "thisIsSuper no call");
    }
}
           

TestSuperPatch.java是DexClassLoader将要加載的代碼:

public class TestSuperPatch {
    public static void testSuperCall() {
        TestSuperClass testSuperClass = new TestSuperClass();
        String t = UUID.randomUUID().toString();
        Log.d("TestSuperPatch", "UUID " + t);
        testSuperClass.setUuid(t);
        testSuperClass.thisIsSuper();
    }
}
           

對TestSuperPatch.class的testSuperClass.thisIsSuper()調用做invokesuper的替換,并且将invokesuper的調用作用在testSuperClass這個對象上,然後加載運作:

報錯資訊說在TestSuperClass和TestSuperClass的父類中沒有找到thisIsSuper()V函數!但是實際上TestSuperClass和父類中是存在thisIsSuper()V函數的,而且通過apk反編譯看也确實存在的,那怎麼就找不到呢?分析invokesuper指令的實作,發現系統會在執行指令所在的class的父類中去找需要調用的方法,是以要将TestSuperPatch跟TestSuperClass一樣作為SuperClass的子類。修改如下:

public class TestSuperPatch extends SuperClass {
    ...
}
           

然後再做一次嘗試:

08-11 09:12:03.012 1787-1787/? D/TestSuperPatch: UUID c5216480-5c3a-4990-896d-58c3696170c5
08-11 09:12:03.012 1787-1787/? D/SuperClass: thisIsSuper c5216480-5c3a-4990-896d-58c3696170c5
           

看一下testSuperCall的實作,将UUID.randomUUID().toString()的結果,通過setUuid指派給了testSuperClass這個對象的父類的uuid字段。從日志可以看出,對testSuperClass.thisIsSuper處理後,确實是調用到了testSuperClass這個對象的super的thisIsSuper函數。OK,super的問題看來解決了,而且這種方式不會增加方法數。

上線後的效果

Robust 靠譜嗎?

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

嘗試修個線上的問題,我們是在07.14下午17:00多的時候上線的更新檔,我們可以看到接下來的幾天一直到07.17号将更新檔下線,這個線上問題得到了明顯的修複,更新檔下線後看到07.18号這個問題又明顯上升了。直到07.18号下班前又重新上線更新檔。

更新檔的相容性和成功率如何?通過以上的理論分析,可以看到這套實作基本沒有相容性問題,實際上線的資料如下:

Android熱更新方案Robust原理插件的問題影響更新檔的問題上線後的效果總結參考文獻

先簡單解釋下這幾個名額:

更新檔清單拉取成功率=拉取更新檔清單成功的使用者/嘗試拉取更新檔清單的使用者

更新檔下載下傳成功率=下載下傳更新檔成功的使用者/更新檔清單拉取成功的使用者

patch應用成功率=patch成功的使用者/更新檔下載下傳成功的使用者

通過這個表能夠看出,我們的patch資訊拉取的成功最低,平均97%多,這是因為實際的網絡原因,而下載下傳成功後的patch成功率是一直在99.8%以上。而且我們做的是無差别下發,服務端沒有做任何針對機型版本的過濾,線上的結果再次證明了Robust的高相容性。

總結

目前業界已有的Android App熱更新方案,包括Multidesk和native hook兩類,都存在一些相容性問題。為此我們借鑒Instant Run原理,實作了一個相容性更強的熱更新方案--Robust。Robust除了高相容性之外,還有實時生效的優勢。so和資源的替換目前暫時未做實作,但是從架構上來說未來是完全有能力支援的。當然,這套方案雖然對開發者是透明的,但畢竟在編譯階段有插件侵入了産品代碼,對運作效率、方法數、包體積還是産生了一些副作用。這也是我們下一步努力的方向。

參考文獻

  • Instant Run, Android Tools Project Site, http://tools.android.com/tech-docs/instant-run.
  • Oracle, The Java Virtual Machine Instruction Set, https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html.
  • Oracle, ClassLoader, https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html).
  • ltshddx, https://github.com/ltshddx/jaop).
  • w4lle, Android熱更新檔之AndFix原了解析.
  • shwenzhang, Android N混合編譯與對熱更新檔影響解析.

https://tech.meituan.com/android_robust.html

繼續閱讀