前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓热修篇-Shadow-思想篇

安卓热修篇-Shadow-思想篇

原创
作者头像
37手游安卓团队
修改2020-11-12 18:19:12
1.2K0
修改2020-11-12 18:19:12
举报
文章被收录于专栏:37手游Android

作者

大家好,我叫小鑫,也可以叫我蜡笔小鑫😊;

本人17年毕业于中山大学,于2018年7月加入37手游安卓团队,曾经就职于久邦数码担任安卓开发工程师;

目前是37手游安卓团队的海外负责人,负责相关业务开发;同时兼顾一些基础建设相关工作

目录

简介

市面上实现插件化的方式大体可分为两种,一种是hook方式,一种是插桩式。其中hook方式,因为需要hook系统API,随着系统API的变化需要不断做适配。因此插桩式方案未来趋势,我更看好代理方式实现的方案

大概步骤

  • 设计标准
  • 开发插件时遵循这个标准
  • 宿主使用自定义的ClassLoader,Resources准备加载插件的环境
  • 在宿主的清单文件用一个空的Activity插桩,加载插件Activity

实现案例

设计标准(可作为一个独立的module,因为宿主和插件需要同一套标准)
代码语言:txt
复制
public interface IActivityInterface {
    public void setAppContext(Activity activity);

    public void onCreate(Bundle bundle);

    public void setContentView(int layoutId);
}
开发插件遵循这套标准(注意,以下只截取了代码片段)
代码语言:txt
复制
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);
    }
}
代码语言:txt
复制
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/dex

PathClassLoader: 只能加载系统中已经安装的apk

同时,由于虚拟机在安装期间会为类打上CLASS_ISPREVERIFIED标志,当满足以下条件时:

在类加载时,由于ClassLoader的双亲委托机制,加载时如果加载了插件中的类了,那么宿主的类便不会再加载而会使用插件的,反之对插件也是一样。这就很容易触发上述所说的verify的问题,从而报出异常“java.lang.IllegalAccessError: Class ref in pre-verified class...”

如何避免?

可以通过自定义ClassLoader修改类加载逻辑,使得插件和宿主中的类隔离,各自加载。

各自加载的好处:插件和宿主依赖的通用模块无需特殊处理。

代码语言:txt
复制
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,无需反射的方式如下:

代码语言:txt
复制
 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中找,代码如下:

代码语言:txt
复制
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
代码语言:txt
复制
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源码

https://juejin.im/post/6870320737794654216

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 作者
  • 目录
  • 简介
  • 大概步骤
  • 实现案例
    • 设计标准(可作为一个独立的module,因为宿主和插件需要同一套标准)
      • 开发插件遵循这套标准(注意,以下只截取了代码片段)
        • 宿主使用自定义的ClassLoader,Resources准备加载插件的环境
          • 宿主中注册一个代理Activity作为容器,加载插件Activity
          • 测试结果
          • Demo源码
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档