android插件化学习

前言

之前看过鸿洋写的滴滴插件化方案 VirtualApk 源码解析,看得有点一头雾水的感觉,觉得作者贴了一堆源码在那,讲了讲思想,或许对那些对插件化已经有比较深了解的人,觉得分析得比较透彻。但对于像我这样对插件化不是很了解的人,还是不大好理解。于是自己又在网上搜了别人写的分析VirtualApk的文章,感觉都大体相似,贴一堆源码了事。
为了更好的掌握插件化,在网上找了好多资料查看,觉得weishu写的分析DroidPlugin,深入理解插件化框架系列对于刚刚入门插件化的小伙伴是一份不可多得的财料。作者的写作思路是先分析四大组件的加载源理,再谈怎么实现插件化。

一、预备知识

当然在学习插件化之前,需要了解以下知识点:

1、Java反射

Java反射可以理解为程序运行时,可以获得任意类的所有属性和方法,可以修改对象的任一属性,调用实例的任一方法。可参考JAVA中的反射机制
如获取类:

//三种获取类的方式:  
Class c1 = Class.forName("Employee");  

Class c2 = Employee.class;  //java中每个类型都有class 属性.  

Employe ee = new Employee();  
Class c3 = e.getClass(); //c3是运行时类 (e的运行时类是Employee) 

Object obj = c3.newInstance(); //调用了Employee的无参数构造方法. 
Field[] fs = c3.getDeclaredFields();  //获取所有的属性  
Field idF = c3.getDeclaredField("id"); //获取id属性
Method setIdM = c3.getMethod("setId", new Class[]{int.class});//获取Employee类的setId()方法
setIdM.setAccessible(true);//如果setId()方法是private,需要设置方法可访问
setIdM.invoke(obj, 11);//调用set方法

2、代理

代理分两种,静态代理和动态代理
静态代理需要为每个类建立一个代理。动态代理可以统一修改代理类的方法,需要先编写代理类,新建实现Invocation处理器类,实现invoke方法,再建立代理类实例,调用相应方法。
动态代理缺点:只可代理接口,不可代理实体类。因为接口不依赖于实现,才能实现代理类的优点。

//要实现的接口
public interface Subject   
{   
  public void doSomething();   
}  
//实际的一个代理类 
public class RealSubject implements Subject   
{   
  public void doSomething()   
  {   
    System.out.println( "call doSomething()" );   
  }   
}
//处理器 
public class ProxyHandler implements InvocationHandler   
{   
  private Object proxied;   

  public ProxyHandler( Object proxied )   
  {   
    this.proxied = proxied;   
  }   

  public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable   
  {   
    //在转调具体目标对象之前,可以执行一些功能处理
    System.out.println("method before");
    //转调具体目标对象的方法
    return method.invoke( proxied, args);
    //在转调具体目标对象之后,可以执行一些功能处理  
    System.out.println("method after");
  }    
} 
//动态代理调用
 RealSubject real = new RealSubject();  
 //调用代理类的newProxyInstance方法,第一个参数是代理类的加载器,第二个参数是一组代理类的接口,第三个参数处理器对象
 Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(), new Class[]{Subject.class}, new ProxyHandler(real));
 proxySubject.doSomething();//会通过运行ProxyHandler的invoke方法,从而调用RealSubject的doSomething方法

有关代理类的实现可以参考彻底理解JAVA动态代理

3、hook机制

hook可以对某个类的所有方法进行改变,如插入一段自己的代码,改变方法实体。
hook机制一般和动态代理一起使用。如我们要捕捉应用的点击click事件,如统计点击次数等。
先了解下android中事件点击的原理,android中所有控件的父类都是View,它会将所有监听对象保存到ListenerInfo类型mListenerInfo对象中,该对象是通过getListenerInfo()方法获取,我们只需要用静态代理对象替换掉mListenerInfo的各种监听的成员变量,就可以达到目的。
如想统计onClick的点击次数,可以新建一个OnClickListener的代理对象:

//自定义的代理事件监听器
    private static class OnClickListenerProxy implements View.OnClickListener {

        private View.OnClickListener object;

        private int count = 0;

        private OnClickListenerProxy(View.OnClickListener object) {
            this.object = object;
        }

        @Override
        public void onClick(View v) {
            count++;
            if (object != null) object.onClick(v);
        }
    }

再通过反射获取到View的ListenerInfo对象的mOnClickListener成员变量,将该成员变量替换成上面的OnClickListenerProxy对象,在适当时间进行hook,即可达到统计目的。具体实现可参考:
Android Hook 全面入侵监听器

4、类加载相关知识

类加载需要明白双亲委派机制,相关类加载知识可以参考我之前写的博文android类加载器ClassLoader

二、插件化讲解

接下来就开始android插件化之旅:
android插件化,其实通俗点说,手机中已安装的应用(宿主)可以动态调用未安装插件。未安装的插件可以是apk、dex、jar包等,想要动态调用未安装的插件,需要解决以下功能。

1、需要解决的问题

(1)插件加载

DexClassLoader可加载未安装的apk\dex\jar包等。

(2)类加载

由于宿主程序和插件的类加载器不是一个,所以宿主无法加载插件的类。

(3)四大组件的加载

activity\service\content provider\boradcast receiver都需要在mainfest中注册,广播也可以动态注册,未注册的组件无法呼起。

(4)资源的加载

插件的资源也会被打包或是混淆,可能与宿主资源id发生冲突。

(5)插件增量更新、后台管理等

按照上面的问题,再去各大插件是如何解决的,或许能让我们更快的学习插件化。

2、插件化各个击破

2.1、插件加载

调用DexClassLoader来加载插件,DexClassLoader调用的是父类BaseDexClassLoader来加载的,我们就不深入探究是如何加载的,只需要知道调用DexClassLoader的构造函数DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)就可以把插件加载到内存中,并可获取到插件的加载器,这四个参数,插件路径,优化后dex的保存路径,插件中包含的library保存路径,父类加载器(即BaseDexClassLoader)。传入相应的参数后就可以把插件加载进来。
如下调用:

DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

2.2、类加载

其实四大组件都可以简单看成一个个的类,如在构造activiy实例时,是通过调用类加载器

ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

加载了需要的Activity类,并通过反射构造出Activity对象。
在加载插件类时,需要插件的类加器将Activity加载到内存中。明白了类加载机制后,我们有两种方法来实现,构造插件的类加载器来加载插件类,或是将插件要加载的类告知宿主加载器,让宿主加载器帮忙加载。

法一:构造插件的类加载器

根据上面构造Activity的实例,使用的类加载器是从r.packageInfo中获取,r.packageInfo是一个LoadedApk对象(APK文件在内存中的表示),我们需要想办法获取到这样一个LoadedApk对象,通过追究源码,,LoadedApk对象是通过ActivityThread的getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)方法生成,它会先在内部mPackages中查找缓存,如果没有则new一个LoadedApk对象,我们可以在启动插件前先把插件的LoadedApk添加到mPackages缓存中,这样就可以在需要加载插件的地方在mPackages中找到插件的LoadedAPk。
实现步骤如下:
1)构建调用getPackageInfoNoCheck方法的第一个参数ApplicationInfo对象,它是AndroidMainfest.xml中的标签,可通过PackageParser提取ApplicationInfo信息,需要调PackageParser.Package的generateApplicationInfo(Package p, int flags, PackageUserState state)方法,第一个参数Package是通过调用PacakgeParser.parsePackage(File apkFile, int flags)方法解析apk获取的。
2)getPackageInfoNoCheck方法的第二个参数PackageUserState代表不同用户中包的信息,反射调用其默认值即可;
3)调用ThreadActivity的getPackageInfoNoCheck方法,构造出的loadedApk对象,通过反射把它添加到ActivityThread的mPackages的map中。
4)hook pms,欺骗PMS插件已安装

法二:委托系统,让系统帮忙加载

宿主程序的ClassLoader最终是继承自BaseDexClassLoader,它是通过DexPathList进行类的查找过程,而此查找通过遍历一个dexElements的数组完成,通过把插件dex添加进这个数组中就可以让宿主ClassLoader获取加载插件类的能力。
参考文章:Android 插件化原理解析——插件加载机制

2.3、四大组件的插件化实现

2.3.1 Activity

由于要启动的Activity必须在mainfest中注册,想要加载插件中的Activity,一般思路是通过在宿主程序中新注册一些预埋的傀儡Activity,再校验目标类前替换成傀儡类,校验完要实例化Activity前,替换成目标类。

(1) 将目标类替换成傀儡类

Activity中生命周期的各种方法都是通过调用Instrumentation中的方法,最终都是通过ActivityManagerNative调用ActivityManagerService中的方法,AMS调用ActivityStack和ActivityStackSupervisor来校验类的合法性等,而AMS属于服务端进程,我们无法进行处理,一般是通过hook ActivityManagerNative的startActivity方法(Droidplugin)或是hook Instrumentation的startActivity方法(VirtualApk),将目标类替换成在宿主AndroidMainfest.xml中注册的代理类。
如下:

public class IActivityManagerHandler implements InvocationHandler{

    private Object mBase;
    public IActivityManagerHandler(Object base) {
        this.mBase = base;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Arrays.toString(args));
        if ("startActivity".equals(method.getName())) {
            Intent raw;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];
            Intent intent = new Intent();
            String targetPackage = "com.smilexie.test";//包名
            ComponentName componentName = new ComponentName(targetPackage, StubActivity.class.getName());//替换成代理类
            intent.setComponent(componentName);
            intent.putExtra(AmsHookHelper.EXTRA_TARGET_INTENT, raw);
            args[index] = intent;
        }
    }
}

还需要利用动态代理,hook掉ActivityManagerNative对象,让它所有方法都转到代理类中。

public static void hookActivitymanagerNative() throws Exception {
        Class<?> activityMangerNativeClass = Class.forName("android.app.ActivityManagerNative");//获取ActivityManagerNative类
        Field gDefaultField = activityMangerNativeClass.getDeclaredField("gDefault");//获取AMN的gDefault属性,其实就是ActivityMangerProxy对象,通过它与服务端的AMS通信
        gDefaultField.setAccessible(true);
        Object gDefault = gDefaultField.get(null);

        Class<?> singleInstanceClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singleInstanceClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);
        Object rawIActivityManager = mInstanceField.get(gDefault);

        Class<?> IActivitityMangerInterface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{IActivitityMangerInterface},
                new IActivityManagerHandler(rawIActivityManager));// 通过动态代理的方式去创建代理对象,之后所有ActivityManagerNative中的方法被调用的时候都会经过这个代理
        mInstanceField.set(gDefault, proxy);//将AMN对象设置成动态代理
    }

上面仅完成了Activity插件化的一半,还需要将傀儡类替换成目标类。

(2) 将傀儡类换回成目标类

在AMS中校验合法性后,AMS要通过ApplicationThreadProxy向客户端的ApplicationThread发送启动类的消息,ApplicationThread向ActivityThread的H类(Handler)发送消息。我们可以hook ActivityThread中的H类的handleMessage方法,处理launchActivity方法里,将代理类替换回目标类。

public class ActivityThreadHandlerCallback implements Handler.Callback {
    Handler mBase;

    public ActivityThreadHandlerCallback(Handler base) {
        this.mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCHE_ACTIVITY:
                handleLaunchActivity(msg);
                break;
        }
        mBase.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        Object obj = msg.obj;
        try {
            Field intent = obj.getClass().getDeclaredField("intent");
            intent.setAccessible(true);
            Intent raw = (Intent) intent.get(obj);
            Intent target = raw.getParcelableExtra(AmsHookHelper.EXTRA_TARGET_INTENT);
            raw.setComponent(target.getComponent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通过反射hook ActivityThread

public static void hookHandlerCallback() throws Exception {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        Object curretnActivitythread = currentActivityThreadMethod.invoke(null);

        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        Handler mH = (Handler) mHField.get(curretnActivitythread);

        Field mCallBackField = Handler.class.getDeclaredField("mCallback");
        mCallBackField.setAccessible(true);
        mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));
    }

参考文章:Activity生命周期管理
注意:在将目标类替换成傀儡类时,还需要考虑Activity的launchMode, theme, intent-filter等,launchMode可以多注册几种不同启动方式的Activity进行占坑,但是intent-filter应该不大好还原。
后面的插件加载尽量不贴代码,尽量把思想给缕清楚。

2.3.2 Service

(1)service的启动原理
service的启动分成两种,一种是调用startService()方法启动一个service,一种是通过其他组件调用bindService()方法绑定一个Service,它的生命周期和组件绑定。bindService()相对startService()方法更复杂一些,它是通过调用ContextImpl.bindService–>bindServiceCommon–>AMN.getDefault().bindService–>AMS.bindService–>ActivityServices.bindServiceLocked–>bringUpServiceLocked(创建Service,会检测Service进程是否启动,已启动直接调用realStartServiceLocked启动Service组件;Service所在进程未启动,则通过startProcessLocked启动新进程)–>ActivityThread.scheduleCreateService–>sendMessage(CREATE_SERVICE)–>handleCreateService(调用类加载器将Service加载进来,创建Context对象,创建Application对象,调用service的onCreate方法)。
(2)service插件化
1) startService方法处理
hook AMN,监听它的startService,替换成占位Service,将启动插件的Service信息传递过去,监听它的stopService方法,将替身替换回目标Service。
2) 创建插件的Service
通过PackageParser.parsePackage方法解析插件AndroidMainfest.xml中的标签,调用PackageParser.generateServiceInfo方法将service转换成ServiceInfo对象,利用ActivityThread.CreateServiceData方法创建Service,并将创建的Service缓存起来。
3) onStart、onStop方法处理
在onStart方法中,获取到上面创建的插件Service对象,执行它的onStart方法。
注意:和Activity启动一样,如果要启动的service所在进程不存在,也需要先启动进程,再进行后面的步骤。
参考文章:Service的管理

2.3.3 contentProvider

contentprovider组件主要用来不同进程间共享数据,实现插件的步骤如下:
(1) 和service类似,插件的contentprovider如果要给系统用,需要应用在安装或是系统启动时,就得把AndroidMainfest下的provider标签下的组件扫描并转成ProviderInfo数组。
(2) ContentProvider安装需要安装才能使用,通过调用ActivityThread的installContentProviders安装ContentProvider;contentProvider安装在很早进行,比Application的onCreate还要早,所以需要在Application的attachBaseContext里进行安装provider。
(3) 在宿主中注册一个StubContentProvider,调用的query\update\delete各方法时调用的是代理类provider的各种方法,但需要要将Uri转成插件的Uri。
参考文章:ContentProvider的管理
插件化实现(四)Service插件化
content Provider是需要安装才能使用,如果contentProvider的进程存在,则直接调用,不存在则

2.3.4 Boradcast Receiver

先了解下广播的注册与发送过程。
(1) 广播的注册:静态注册直接在AndroidMainfest.xml中进行注册,动态注册最终调用的AMS.registerReceiver()方法,在进行插件化的时候,需要静态转动态注册。
(2) 广播的发送:一般是调用context.sendBoradcast —> AMN.getDefault().boradcastIntent
在AMP中boradcastIntent中方法会查询静态的广播列表receivers和动态注册的广播列表mReceiverResolver,调用scheduleBroadcastLocked()通知队列对广播进行处理,再调用receiver.performReceiver()方法将广播发送出去。最终会调用ActivityThread.post(Agrs),Args是一个Runnable对象,内部的run方法里,会回调receiver.onReceive方法。
(3) 广播插件化:
1) 静态广播转动态广播注册
通过PackageParser解析APK中的AndroidMainfest.xml,获取标签的receivers集合,将receiver–>ActivityInfo对象,再利用插件的加载器将BroadcastReceiver类加载进来,并调用context.registerReceiver方法进行广播的注册;
2) 动态广播注册
直接利用插件的加载器将BroadcastReceiver类加载进来,调用context.registerReceiver方法进行广播的注册;
3) hook ActivityManagerNatice的registerReceiver方法,将BroadcastReceiver放到宿主ProcessRecord进程里
由于AMS.registerReceiver(IApplicationThread caller, String callerPackage, IIntentReceiver receiver, IntentFilter filter, String permission, int userId)方法中callerPackage应该是宿主的包名,IIntentReceiver是一个接口,实现类位于LoadedApk内部类的ReceiverDispatcher的内部类InnerReceiver,我们需要反射调用LoadedApk的getReceiverDispatcher方法,生成新的IIntentReceiver,替换原来的,从而达到将广播放到原宿主过程中目的。
参考文章:
插件化实现(五)BroadcastReceiver插件化
广播的处理方式

2.4、资源加载

1)将apk资源解压出来,通过操作文件的方式来使用,理论上可行,但实际使用起来有问题(如资源混淆问题);
2)通过AssetManager的addAssetPath()方法,将插件资源加载进入AssetManager变量中,并创建Resources对象。重写Context的getResources()getAsset()等方法,资源冲突时,需要扩展aapt;如打包执行一个脚本修改资源ID。
资源id在编译时生成,生成0xPPTTNNNN(PP标记apk,系统资源PP是01,应用程序的PP是07;TT标记资源类型,如图标、布局等;NNNN是某种资源类型的资源id,默认从1开始),每次编译时TT值固定,可达到资源id分组效果,从而避免重复。

2.5、插件增量更新、后台管理

提供插件信息查询和下载,包括回滚到某一版本;
管理使用插件apk,可以向不同版本的apk提供不同插件;
MD5校验插件合法性

总结

这段时间研究插件化,对各组件的启动流程也有一定了解,都是通过AMN与AMS通信,通过AMS来启动各组。实现插件化大部分都是从AMS处做文章。
(1)Activity的插件化主要是在Mainfest中注册几个替身,hook Instrumentation或是AMN.getDefault的startActivity,Activity信息校验完,需要hook ActivityThread的内部类H的mCallback对象,在handleMessage中将替身换回目标Activity。
(2)Service的插件化,hook AMN.getDefault的startService方法,替换成占位Service,在占位Service的onStart方法中,调用插件的service对象进行处理相应的操作;同时还要hook stopService方法,需要将替身转成真身调用其destory方法。
(3)ContentProvider插件化,解析package包中AndroidMainfest.xml,保存providers并将每一个provider转成ProviderInfo,生成ProviderInfos数组,再调用ActivityThread的installContentProviders安装ContentProvider。之后在宿主程序中注册一个StubProvider,之后ContentProvider的所有操作都会走到StubProvider,只需要将uri变成目标contentProvider的就行。
(4)BroadcastReceiver插件化,静态转动态注册,同时需要将boradcastReceiver添加进宿主进程。
(5)资源的插件化,可通过AssetManager的addAssetPath方法,将插件资源添加进宿主的path里,如果发生资源冲突,可以修改资源id来解决。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值