Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

文章目錄

1 熱修複背景

剛釋出的版本出現了嚴重的Bug,這就需要去解決Bug、測試打包重新釋出,這會耗費大量的人力和物力,代價比較大。

已經更正了此前釋出版本的Bug,如果下個版本是大版本,那麼兩個版本之間間隔時間會很長,這樣要等到下個大版本釋出再修複Bug,而之前版本的Bug會長期的影響使用者。

版本更新率不高,并且需要長時間來完成版本疊代,前版本的Bug就會一直影響不更新的使用者。

有一些小但是很重要的功能需要在短時間内完成版本疊代,比如節日活動。

正常開發流程與熱修複開發流程對比:

Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

熱修複架構分類與對比:

Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

分類:

阿裡系:AndFix、Dexposed、阿裡百川、Sophix

騰訊系:微信的Tinker、QQ空間的超級更新檔、手Q的QFix

知名公司:美團的Robust、餓了麼的Amigo、美麗說蘑菇街的Aceso

其它:RocooFix、Nuwa、AnoleFix

2 代碼修複

2.1 底層替換方案

在已加載的類中直接替換原有方法,是在原有類的基礎上進行修改,無法實作對原有類進行方法和字段的增減,這樣會破壞原有類的結構。

最大問題是不穩定性,直接修改虛拟機方法實體的具體字段來實作的。Android是開源的,不同的手機廠商開源對代碼進行修改,是以像Andfix就會出現在部分機型上的熱修複失效的現象。

修改ArtMethod

Sophix:全部替換底層的,

2.2 類加載方案

APP重新啟動後,讓ClassLoader去加載新的類。

熱修複優勢:

無需重新釋出新版本,省時省力。

使用者無感覺修複,也無需下載下傳最新應用,代價小。

修複成功率高,把損失降到最低。

ClassLoader classLoader = MainActivity.class.getClassLoader();
        while (classLoader != null){
            Log.i("MainActivity123",classLoader.toString());
            classLoader = classLoader.getParent();
        }
           

輸出:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hongx.fixtest-
iWkvfdNvIRqzj51aylqjJg==/base.apk"],nativeLibraryDirectories=
[/data/app/com.hongx.fixtest-iWkvfdNvIRqzj51aylqjJg==/lib/x86, /system/lib]]]
           

[email protected]
           
Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

先看看DexClassLoader和PathClassLoader的差別

參考:https://www.jianshu.com/p/7e30ba5cb9ea

1、DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk

2、PathClassLoader隻能加載系統中已經安裝過的apk

再看看ClassLoader的loadClass方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
           
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }
           
protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }
           

findLoadedClass可以看出ClassLoader先去加載的是已加載過的,如果未加載過(c == null)時候才會走,則去所有需要冷啟動重新啟動app後才能生效。

//pathc.dex 放在私有目錄下

Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

3 插樁原理

http://androidxref.com/8.1.0_r33/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

Tinker熱修複原理1 熱修複背景2 代碼修複3 插樁原理4 Dex分包5 未來發展展望

4 Dex分包

4.1 65536限制:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

應用的方法數超過了最大數65536個。因為DVM Bytecode的限制,DVM指令集的方法調用指令invoke-kind索引為16bits,最多能引用65535個方法

4.2 LinearAlloc限制

INSTALL_FAILED_DEXOPT

在安裝應用時可能會提示INSTALL_FAILED_DEXOPT,産生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一個固定的緩存區,當方法數超出緩存區的大小時會報錯。

為了解決65536限制和LinearAlloc限制,進而産生了Dex分包機制。

Dex分包方案主要做的時在打包時将應用代碼分成多個Dex,将應用啟動時必須用到的類和這些類的直接引用類放到主Dex中,其它代碼放到次Dex中。當應用啟動時先加載主Dex,等到應用啟動後再動态地加載次Dex,進而緩解了主Dex的65536限制和LinearAlloc限制

4.3 gradle配置

android {
	compileSdkVersion 28
	defaultConfig {
		...
		 // 開啟分包
		multiDexEnabled true
		// 設定分包配置檔案
		multiDexKeepFile file('multidex.keep')
	}
	...
	dexOptions {
		javaMaxHeapSize "4g"
		preDexLibraries = false
		additionalParameters = [ // 配置multidex參數
                                 '--multi-dex', // 多dex分包
                                 '--set-max-idx-number=50000', // 每個包内方法數上限
                                 '--main-dex-list=' + '/multidex-config.txt', // 打包到主classes.dex的檔案清單
                                 '--minimal-main-dex'
		]
	}
}
dependencies {
    ...
    implementation 'com.android.support:multidex:1.0.3'
}
           

4.4 配置檔案multidex.keep

com/hongx/fixtest/BaseActivity.class

com/hongx/fixtest/BaseApplication.class

com/hongx/fixtest/MainActivity.class

4.5 Application重寫attachiBaseContext

@Override
protected void attachBaseContext(Context base) {
	super.attachBaseContext(base);
	MultiDex.install(this);
}
           

Make Project 後檢視app/build/outputs/apk/debug/app-debug.apk 多出一個classes.dex裡面包含了MainActivity、BaseApplication、BaseActivity

5 未來發展展望

熱修複=“黑科技”?

熱修複不同于國内APP程序保活這種“黑科技”,讓app常駐背景,既耗電又占用記憶體,浪費很多手機資源。還有APP的推送服務,無節操地對使用者進行資訊轟炸。還有更無節操的全家桶app。導緻Android手機卡頓不堪,這些所謂的“黑科技”都是為了手機廠商的利益而損害使用者的體驗

而熱修複是能夠讓開發者和使用者雙赢的。不僅廠商能快速疊代更新app,使功能盡快上線,而且熱更新過程使用者無感覺,節省大量更新時間,提高使用者體驗。更重要的能保證app的功能穩定,bug能及時修複。

IOS封殺了熱修複功能,Android的熱修複是否也有可能被幹掉呢?

熱修複未來是十分樂觀的,不僅不會受到封殺,反而會有很大發展空間

Android熱修複 android 熱修複 Tinker

P6091-[模闆]原根

正題

題目連結:https://www.luogu.com.cn/problem/P6091

題目大意

給出一個數\(p\),求出它的所有在\([0,p]\)的原根。

解題思路

原根的定義,\(\delta_p(a)\)表示一個最小的\(n\)使得\(a^n\equiv1(mod\ p)\),若\(gcd(a,p)=1\)且\(\delta_p(a)=\varphi(p)\)則\(a\)為\(p\)的一個原根。

兩個個結論就是一個數有原根當且僅當它為\(2,4,p^a,2p^a\)(其中\(p\)為奇質數,\(a\in N^+\))。還有若\(g\)表示最小正原根,那麼其他原根可以被表示為\(g^k\% p(\ gcd(\varphi(p),k)=1\ )\)。

這兩個結論在洛谷題解都有詳細證明,這裡就不多贅述了。

那麼考慮如何求出最小正原根,因為原根的數量大約有\(\varphi(\varphi (p))\)個,是以密集度比較高,據說最小正原根約是\(O(n^{2.5})\)級别的。

是以考慮直接枚舉,但是我們判定的時候肯定不能從\(1\sim \varphi(p)\)枚舉來判斷。

我們還需要用到一個結論就是如果對于\(gcd(a,p)=1\)且\(a^k\equiv 1(mod\ p)\)(也就是\(k\)是\(a\)模\(n\)的階),那麼有\(k|\varphi(p)\)。是以我們需要判定\(\varphi(p)\)的所有因子?看起來還是很大,但是我們顯然有\(a^k\equiv 1(mod\ p)\)那麼\(a^{kx}\equiv1(mod\ p)\)其中\(x\in N^+\)。是以我們隻需要枚舉\(\frac{\varphi(p)}{k}\)(其中\(k\)是\(\varphi(p)\)的質因子)即可,因為這些數包含了其他數的倍數。

時間複雜度\(O(n^{2.5}\log n+n\log n)\)

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define ll long long
using namespace std;
const ll N=1e6+10;
ll T,n,d,cnt,phi[N],pri[N];
bool v[N],rt[N];
vector<int> q;
void prime(){
    phi[1]=1;
    for(ll i=2;i<N;i++){
        if(!v[i])pri[++cnt]=i,phi[i]=i-1;
        for(ll j=1;j<=cnt&&i*pri[j]<N;j++){
            v[i*pri[j]]=1;
            if(i%pri[j]==0){
                phi[i*pri[j]]=phi[i]*pri[j];
                break;
            }
            phi[i*pri[j]]=phi[i]*phi[pri[j]];
        }
    }
    rt[2]=rt[4]=1;
    for(ll i=2;i<=cnt;i++){
        for(ll j=1;j<N;j*=pri[i])rt[j]=1;
        for(ll j=2;j<N;j*=pri[i])rt[j]=1;
    }
    return;
}
ll power(ll x,ll b,ll p){
    ll ans=1;
    while(b){
        if(b&1)ans=ans*x%p;
        x=x*x%p;b>>=1;
    }
    return ans;
}
ll gcd(ll x,ll y)
{return (!y)?x:gcd(y,x%y);}
void dec_phi(ll x){
    for(ll i=1;i<=cnt&&pri[i]*pri[i]<=x;i++)
        if(x%pri[i]==0){
            q.push_back(pri[i]);
            while(x%pri[i]==0)x/=pri[i];
        }
    if(x!=1)q.push_back(x);
    return;
}
bool check(ll x){
    if(power(x,phi[n],n)!=1)return 0;
    for(ll i=0;i<q.size();i++)
        if(power(x,phi[n]/q[i],n)==1)
            return 0;
    return 1;
}
signed main()
{
    scanf("%lld",&T);
    prime();
    while(T--){
        scanf("%lld%lld",&n,&d);q.clear();
        if(!rt[n]){printf("0\n\n");continue;}
        dec_phi(phi[n]);ll g=1;
        while(!check(g))g++;
        ll tmp=1;q.clear();
        for(ll i=1;i<=phi[n];i++){
            tmp=tmp*g%n;
            if(gcd(phi[n],i)==1)
                q.push_back(tmp);
        }
        printf("%lld\n",q.size());
        sort(q.begin(),q.end());
        for(ll i=1;i<=q.size()/d;i++)
            printf("%lld ",q[i*d-1]);
        putchar('\n');
    }
    return 0;
}