安卓热修系列-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

相关文章
相关标签/搜索