前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >插件化三问—字节真题

插件化三问—字节真题

作者头像
码上积木
发布2020-10-29 16:42:27
7030
发布2020-10-29 16:42:27
举报
文章被收录于专栏:码上积木

提到免安装应用,大家肯定第一想到的就是小程序,但是在Android中其实是有这么一项技术用于动态加载apk的,那就是插件化。今天一起来看看吧!

  • 为什么需要插件化
  • 插件化的原理
  • 市面上的一些插件化方案以及你的想法

为什么需要插件化

我觉得最主要的原因是可以动态扩展功能。把一些不常用的功能或者模块做成插件,就能减少原本的安装包大小,让一些功能以插件的形式在被需要的时候被加载,也就是实现了动态加载

比如动态换肤、节日促销、见不得人的一些功能,就可以在需要的时候去下载相应模式的apk,然后再动态加载功能。所以一般这个功能适用于一些平台类的项目,比如大众点评美团这种,功能很多,用户很大概率只会用其中的一些功能,而且这些模块单独拿出来都可以作为一个app运行。

但是现在用的缺很少了,具体情况见第三点。

插件化的原理

要实现插件化,也就是实现从apk读取所有数据,要考虑三个问题:

  • 读取插件代码,完成插件中代码的加载和与主工程的互相调用
  • 读取插件资源,完成插件中资源的加载和与主工程的互相访问
  • 四大组件管理

1)读取插件代码,其实也就是进行插件中的类加载。所以用到类加载器就可以了。Android中常用的有两种类加载器,DexClassLoaderPathClassLoader,它们都继承于BaseDexClassLoader。区别在于DexClassLoader多传了一个optimizedDirectory参数,表示缓存我们需要加载的dex文件的,并创建一个DexFile对象,而且这个路径必须为内部存储路径。而PathClassLoader这个参数为null,意思就是不会缓存到内部存储空间了,而是直接用原来的文件路径加载。所以DexClassLoader功能更为强大,可以加载外部的dex文件。

同时由于双亲委派机制,在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。而主工程调用插件则需要通过DexClassLoader去加载类,然后反射调用方法。

2)读取插件资源,主要是通过AssetManager进行访问。具体代码如下:

代码语言:javascript
复制
/**
 * 加载插件的资源:通过AssetManager添加插件的APK资源路径
 */
protected void loadPluginResources() {
    //反射加载资源
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }
    Resources superRes = super.getResources();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}   

通过addAssetPath方法把插件的路径穿进去,就可以访问到插件的资源了。

3)四大组件管理 为什么单独说下四大组件呢?因为四大组件不仅要把他们的类加载出来,还要去管理他们的生命周期,在AndroidManifest.xml中注册。这也是插件化中比较重要的一部分。这里重点说下Activity。

主要实现方法是通过Hook技术,主要的方案就是先用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity替换占坑的Activity

❝Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。 ❞

这里的hook其实就是我们常说的下钩子,可以改变函数的内部行为。

这里加载插件Activity用到hook技术,有两个可以hook的点,分别是:

  • Hook IActivityManager 上面说了,首先会在AndroidManifest.xml中注册的Activity来进行占坑,然后合适的时机来替换我们要加载的Activity。所以我们主要需要两步操作:第一步:使用占坑的这个Activity完成AMS验证。也就是让AMS知道我们要启动的Activity是在xml里面注册过的哦。具体代码如下:
代码语言:javascript
复制
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("startActivity".contains(method.getName())) {
                //换掉
                Intent intent = null;
                int index = 0;
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];
                    if (arg instanceof Intent) {
                        //说明找到了startActivity的Intent参数
                        intent = (Intent) args[i];
                        //这个意图是不能被启动的,因为Acitivity没有在清单文件中注册
                        index = i;
                    }
                }
               //伪造一个代理的Intent,代理Intent启动的是proxyActivity
                Intent proxyIntent = new Intent();
                ComponentName componentName = new ComponentName(context, proxyActivity);
                proxyIntent.setComponent(componentName);
                proxyIntent.putExtra("oldIntent", intent);
                args[index] = proxyIntent;
            }

            return method.invoke(iActivityManagerObject, args);
        }

第二步:替换回我们的Activity。上面一步是把我们实际要启动的Activity换成了我们xml里面注册的activity来躲过验证,那么后续我们就需要把Activity换回来。Activity启动的最后一步其实是通过H(一个handler)中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。最后会调用到Handler的dispatchMessage方法用于处理消息,如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。所以我们能hook的点就是这个mCallback

代码语言:javascript
复制

 public static void hookHandler() throws Exception {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
        Field mHField = FieldUtil.getField(activityThread,"mH");//2
        Handler mH = (Handler) mHField.get(currentActivityThread);//3
        FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
    }

public class HCallback implements Handler.Callback{
    //...
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到消息中的Intent(启动SubActivity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //得到此前保存起来的Intent(启动TargetActivity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //将启动SubActivity的Intent替换为启动TargetActivity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

用自定义的HCallback来替换mH中的mCallback即可完成Activity的替换了。

  • Hook Instrumentation

这个方法是由于startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期,所以可以通过替换Instrumentation来完成,然后在InstrumentationexecStartActivity方法中用占坑SubActivity来通过AMS的验证,在InstrumentationnewActivity方法中还原TargetActivity。

代码语言:javascript
复制
public class InstrumentationProxy extends Instrumentation {
    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;
    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
        mInstrumentation = instrumentation;
        mPackageManager = packageManager;
    }
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        if (infos == null || infos.size() == 0) {
            intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
            intent.setClassName(who, "com.example.liuwangshu.pluginactivity.StubActivity");//2
        }
        try {
            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
                    target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
        IllegalAccessException, ClassNotFoundException {
     String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
     if (!TextUtils.isEmpty(intentName)) {
         return super.newActivity(cl, intentName, intent);
     }
     return super.newActivity(cl, className, intent);
 }

}

  public static void hookInstrumentation(Context context) throws Exception {
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        Field mMainThreadField  =FieldUtil.getField(contextImplClass,"mMainThread");//1
        Object activityThread = mMainThreadField.get(context);//2
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
        FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
                context.getPackageManager()));
    }

市面上的一些插件化方案以及你的想法

前几年插件化还是很火的,比如Dynamic-Load-Apk(任玉刚),DroidPlugin,RePlugin(360),VirtualApk(滴滴),但是现在机会都没怎么在运营了,好多框架都只支持到Android9。

这是为什么呢?我觉得一个是维护成本太高,每更新一次源码,就要重新维护一次。二就是确实插件化技术现在用的不多了,以前用插件化干嘛?主要是更新代码,修复bug。那么现在又热更新技术了,为什么还还要考虑插件化呢?组件化+热更新就完全够用了。

虽然插件化用的不多了,但是我觉得技术还是可以了解的,而且热更新主要用的也是这些技术。方案可以被淘汰,但是技术不会。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上积木 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要插件化
  • 插件化的原理
  • 市面上的一些插件化方案以及你的想法
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档