安卓熱修系列-Shadow-思想篇

做者

你們好,我叫小鑫,也能夠叫我蠟筆小鑫😊;java

本人17年畢業於中山大學,於2018年7月加入37手遊安卓團隊,曾經就任於久邦數碼擔任安卓開發工程師;android

目前是37手遊安卓團隊的海外負責人,負責相關業務開發;同時兼顧一些基礎建設相關工做微信

目錄

簡介 市面上實現插件化的方式大致可分爲兩種,一種是hook方式,一種是插樁式。其中hook方式,由於須要hook系統API,隨着系統API的變化須要不斷作適配。所以插樁式方案將來趨勢,我更看好代理方式實現的方案post

大概步驟

  • 設計標準
  • 開發插件時遵循這個標準
  • 宿主使用自定義的ClassLoader,Resources準備加載插件的環境
  • 在宿主的清單文件用一個空的Activity插樁,加載插件Activity

實現案例

設計標準(可做爲一個獨立的module,由於宿主和插件須要同一套標準)

public interface IActivityInterface {
    public void setAppContext(Activity activity);

    public void onCreate(Bundle bundle);

    public void setContentView(int layoutId);
}
複製代碼

開發插件遵循這套標準(注意,如下只截取了代碼片斷)

public class BaseActivity implements IActivityInterface {

    private Activity mActivity;

    @Override
    public void setAppContext(Activity activity) {
        Log.i("我是插件", "setAppContext");
        mActivity = activity;
    }

    @Override
    public void onCreate(Bundle bundle) {
        Log.i("我是插件", "onCreate");
    }

    @Override
    public void setContentView(int layoutId) {
        Log.i("我是插件", "setContentView");
        mActivity.setContentView(layoutId);
    }
}
複製代碼
public class PluMainActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_plu);
    }

}
複製代碼

宿主使用自定義的ClassLoader,Resources準備加載插件的環境

  • 1)ClassLoader的處理

Android中的ClassLoader類加載器派生出的有DexClassLoader和PathClassLoader。這二者的區別是測試

DexClassLoader: 可以加載未安裝的jar/apk/dexui

PathClassLoader: 只能加載系統中已經安裝的apkthis

同時,因爲虛擬機在安裝期間會爲類打上CLASS_ISPREVERIFIED標誌,當知足如下條件時:

在類加載時,因爲ClassLoader的雙親委託機制,加載時若是加載了插件中的類了,那麼宿主的類便不會再加載而會使用插件的,反之對插件也是同樣。這就很容易觸發上述所說的verify的問題,從而報出異常「java.lang.IllegalAccessError: Class ref in pre-verified class...」

如何避免?

能夠經過自定義ClassLoader修改類加載邏輯,使得插件和宿主中的類隔離,各自加載。

各自加載的好處:插件和宿主依賴的通用模塊無需特殊處理。

package com.sq.a37syplu10.plugin.loader;

import android.os.Build;

import dalvik.system.DexClassLoader;

public class ApkClassLoader extends DexClassLoader {

    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    public ApkClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent, String[] interfacePackageNames) {

        super(dexPath, optimizedDirectory, librarySearchPath, parent);

        ClassLoader grand = parent;
        mGrandParent = grand.getParent();
        this.mInterfacePackageNames = interfacePackageNames;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        String packageName;
        int dot = className.lastIndexOf('.');
        if (dot != -1) {
            packageName = className.substring(0, dot);
        } else {
            packageName = "";
        }

        boolean isInterface = false;
        for (String interfacePackageName : mInterfacePackageNames) {
            if (packageName.equals(interfacePackageName)) {
                isInterface = true;
                break;
            }
        }

        if (isInterface) {
            return super.loadClass(className, resolve);
        } else {
            Class<?> clazz = findLoadedClass(className);

            if (clazz == null) {
                ClassNotFoundException suppressed = null;
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    suppressed = e;
                }

                if (clazz == null) {
                    try {
                        clazz = mGrandParent.loadClass(className);
                    } catch (ClassNotFoundException e) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            e.addSuppressed(suppressed);
                        }
                        throw e;
                    }
                }
            }

            return clazz;
        }
    }

    /** * 從apk中讀取接口的實現 * * @param clazz 接口類 * @param className 實現類的類名 * @param <T> 接口類型 * @return 所需接口 * @throws Exception */
    public <T> T getInterface(Class<T> clazz, String className) throws Exception {
        try {
            Class<?> interfaceImplementClass = loadClass(className);
            Object interfaceImplement = interfaceImplementClass.newInstance();
            return clazz.cast(interfaceImplement);
        } catch (ClassNotFoundException | InstantiationException
                | ClassCastException | IllegalAccessException e) {
            throw new Exception(e);
        }
    }

}

複製代碼

上述代碼中,除了隔離宿主和插件的類加載外,還預留了白名單。由於宿主和插件中,遵循同一套標準時,就須要將插件中加載的類,轉爲宿主的標準的類型。根據同一個類加載器加載且全類名相同纔算同一個類,須要用父加載器加載的接口才能夠進行類型轉換。所以須要將IActivityInterface列入白名單。

同時,因爲插件中的類也存在verify的問題,BaseActivity引用了IActivityInterface,而且BaseActivity引用的類都屬於一個dex,BaseActivity會被打上標識。那麼當使用宿主的IActivityInterface時,就會 報錯。

那麼,怎麼解決?

將插件中的標準處理成jar包,使用compileOnly方式依賴,不打入插件apk中。這樣BaseActivity便不會被打上標識,問題解決。即宿主和插件中須要經過接口類型轉換的,將插件中該接口去除。

  • 2)處理Resources

常規方案:

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, mPluginPath);
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
複製代碼

缺點1:使用了反射,而且addAssetPath方法已經廢棄,甚至在高版本中已經不存在該方法了

缺點2:只使用插件的Resouces,宿主的setContentView方法前的其餘資源加載不到,日誌中會有異常報出support包相關的資源找不到。

採用騰訊shadow中的方案:

第一步,加載插件中的resources,無需反射的方式以下:

private Resources buildPluginResources() {
        try {
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
            mContext.getPackageName(),
                    PackageManager.GET_ACTIVITIES
                            | PackageManager.GET_META_DATA
                            | PackageManager.GET_SERVICES
                            | PackageManager.GET_PROVIDERS
                            | PackageManager.GET_SIGNATURES);
            packageInfo.applicationInfo.publicSourceDir = mPluginPath;
            packageInfo.applicationInfo.sourceDir = mPluginPath;
            return mContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
複製代碼

第二步,利用宿主包的Resouces和插件包的Resouces混合出一個新的Resources。獲取資源時,先搜索插件的Resouces,若是找不到,則從宿主Resouces中找,代碼以下:

package com.sq.a37syplu10.plugin.resources;

import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Movie;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.TypedValue;
import java.io.InputStream;

/** * Resources資源先從插件獲取,若是獲取不到則從宿主獲取 */
public class MixResources extends ResourcesWrapper {

    private Resources mHostResources;

    public MixResources(Resources hostResources, Resources pluginResources) {
        super(pluginResources);
        mHostResources = hostResources;
    }

    @Override
    public CharSequence getText(int id) throws NotFoundException {
        try {
            return super.getText(id);
        } catch (NotFoundException e) {
            return mHostResources.getText(id);
        }
    }

    @Override
    public String getString(int id) throws NotFoundException {
        try {
            return super.getString(id);
        } catch (NotFoundException e) {
            return mHostResources.getString(id);
        }
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        try {
            return super.getString(id,formatArgs);
        } catch (NotFoundException e) {
            return mHostResources.getString(id,formatArgs);
        }
    }

    @Override
    public float getDimension(int id) throws NotFoundException {
        try {
            return super.getDimension(id);
        } catch (NotFoundException e) {
            return mHostResources.getDimension(id);
        }
    }

    @Override
    public int getDimensionPixelOffset(int id) throws NotFoundException {
        try {
            return super.getDimensionPixelOffset(id);
        } catch (NotFoundException e) {
            return mHostResources.getDimensionPixelOffset(id);
        }
    }

    @Override
    public int getDimensionPixelSize(int id) throws NotFoundException {
        try {
            return super.getDimensionPixelSize(id);
        } catch (NotFoundException e) {
            return mHostResources.getDimensionPixelSize(id);
        }
    }

    @Override
    public Drawable getDrawable(int id) throws NotFoundException {
        try {
            return super.getDrawable(id);
        } catch (NotFoundException e) {
            return mHostResources.getDrawable(id);
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
        try {
            return super.getDrawable(id, theme);
        } catch (NotFoundException e) {
            return mHostResources.getDrawable(id,theme);
        }
    }

    @Override
    public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
        try {
            return super.getDrawableForDensity(id, density);
        } catch (NotFoundException e) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                return mHostResources.getDrawableForDensity(id, density);
            } else {
                return null;
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawableForDensity(int id, int density, Theme theme) {
        try {
            return super.getDrawableForDensity(id, density, theme);
        } catch (Exception e) {
            return mHostResources.getDrawableForDensity(id,density,theme);
        }
    }

    @Override
    public int getColor(int id) throws NotFoundException {
        try {
            return super.getColor(id);
        } catch (NotFoundException e) {
            return mHostResources.getColor(id);
        }
    }
    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public int getColor(int id, Theme theme) throws NotFoundException {
        try {
            return super.getColor(id,theme);
        } catch (NotFoundException e) {
            return mHostResources.getColor(id,theme);
        }
    }

    @Override
    public ColorStateList getColorStateList(int id) throws NotFoundException {
        try {
            return super.getColorStateList(id);
        } catch (NotFoundException e) {
            return mHostResources.getColorStateList(id);
        }
    }
    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
        try {
            return super.getColorStateList(id,theme);
        } catch (NotFoundException e) {
            return mHostResources.getColorStateList(id,theme);
        }
    }

    @Override
    public boolean getBoolean(int id) throws NotFoundException {
        try {
            return super.getBoolean(id);
        } catch (NotFoundException e) {
            return mHostResources.getBoolean(id);
        }
    }

    @Override
    public XmlResourceParser getLayout(int id) throws NotFoundException {
        try {
            return super.getLayout(id);
        } catch (NotFoundException e) {
           return mHostResources.getLayout(id);
        }
    }

    @Override
    public String getResourceName(int resid) throws NotFoundException {
        try {
            return super.getResourceName(resid);
        } catch (NotFoundException e) {
            return mHostResources.getResourceName(resid);
        }
    }

    @Override
    public int getInteger(int id) throws NotFoundException {
        try {
            return super.getInteger(id);
        } catch (NotFoundException e) {
            return mHostResources.getInteger(id);
        }
    }

    @Override
    public CharSequence getText(int id, CharSequence def) {
        try {
            return super.getText(id,def);
        } catch (NotFoundException e) {
            return mHostResources.getText(id,def);
        }
    }

    @Override
    public InputStream openRawResource(int id) throws NotFoundException {
        try {
            return super.openRawResource(id);
        } catch (NotFoundException e) {
            return mHostResources.openRawResource(id);
        }

    }

    @Override
    public XmlResourceParser getXml(int id) throws NotFoundException {
        try {
            return super.getXml(id);
        } catch (NotFoundException e) {
            return mHostResources.getXml(id);
        }
    }

    @TargetApi(Build.VERSION_CODES.O)
    @Override
    public Typeface getFont(int id) throws NotFoundException {
        try {
            return super.getFont(id);
        } catch (NotFoundException e) {
            return mHostResources.getFont(id);
        }
    }

    @Override
    public Movie getMovie(int id) throws NotFoundException {
        try {
            return super.getMovie(id);
        } catch (NotFoundException e) {
            return mHostResources.getMovie(id);
        }
    }

    @Override
    public XmlResourceParser getAnimation(int id) throws NotFoundException {
        try {
            return super.getAnimation(id);
        } catch (NotFoundException e) {
            return mHostResources.getAnimation(id);
        }
    }

    @Override
    public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
        try {
            return super.openRawResource(id,value);
        } catch (NotFoundException e) {
            return mHostResources.openRawResource(id,value);
        }
    }

    @Override
    public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
        try {
            return super.openRawResourceFd(id);
        } catch (NotFoundException e) {
            return mHostResources.openRawResourceFd(id);
        }
    }
}

複製代碼

宿主中註冊一個代理Activity做爲容器,加載插件Activity

package com.sq.a37syplu10.plugin;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import com.sq.a37syplu10.MainActivity;
import com.sq.a37syplu10.plugin.loader.ApkClassLoader;
import com.sq.aninterface.IActivityInterface;

public class ProxyPluginActivity extends Activity {

    @Override
    public ApkClassLoader getClassLoader() {
        return MainActivity.mPlugin.mClassLoader;
    }

    @Override
    public Resources getResources() {
        return MainActivity.mPlugin.mResource;
    }

    private IActivityInterface pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();

        if (intent != null && !TextUtils.isEmpty(intent.getStringExtra("activity"))) {
            try {
                pluginActivity = getClassLoader().getInterface(IActivityInterface.class, intent.getStringExtra("activity"));
                pluginActivity.setAppContext(this);
                pluginActivity.onCreate(new Bundle());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            Log.e("我是宿主", "intent 中沒帶插件activity信息");
        }
    }


    @Override
    public void startActivity(Intent intent) {
        if (!TextUtils.isEmpty(intent.getStringExtra("activity"))) {
            intent.setClass(this, ProxyPluginActivity.class);
        }
        super.startActivity(intent);
    }
}

複製代碼

測試結果

經測試,模擬器,真機從android4-10都正常。暫無遇到兼容問題

Demo源碼

juejin.cn/post/687032…

結束語

過程當中有問題或者須要交流的同窗,能夠掃描二維碼加好友,而後進羣進行問題和技術的交流等;

企業微信截圖_5d79a123-2e31-42cc-b03f-9312b8b99df3.png

相關文章
相關標籤/搜索