精华内容
下载资源
问答
  • 【Android 插件化插件化简介 ( 组件化与插件化 )

    千次阅读 多人点赞 2021-05-29 21:27:36
    一、组件化与插件化、 二、插件化示例、 三、插件化标准引入、

    Android 插件化系列文章目录

    【Android 插件化】插件化简介 ( 组件化与插件化 )
    【Android 插件化】插件化原理 ( JVM 内存数据 | 类加载流程 )
    【Android 插件化】插件化原理 ( 类加载器 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 原理与实现思路 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 类加载器创建 | 资源加载 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 注入上下文的使用 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 获取插件入口 Activity 组件 | 加载插件 Resources 资源 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 运行应用 | 代码整理 )






    一、组件化与插件化



    组件化 是将应用分成若干 Module 模块 , 每个模块称为一个组件 ;

    组件化 项目中 , 分为两种模式 , " 集成模式 " " 组件模式 " ;

    在开发过程中的 " 组件模式 " 下这些组件可以 独立运行 , 在 " 集成模式 " 下 , 这些组件 相互依赖拼装成一个 APK 安装包 ;


    组件化开发的弊端 :

    多个模块必须是并发开发 , 模块之间相互依赖 , 如果修改了一个模块 , 那就必须重新打包 ;

    插件化开发 , 解决了上述问题 ;


    插件化 将应用拆分成若干模块 , 其中有 11" 宿主 " 模块 , 若干 " 插件 " 模块 ;

    最终打包时 , 将 " 宿主 " 模块 和 " 插件 " 模块 分开进行打包 ;

    " 宿主 " 模块" 插件 " 模块 都各自是一个单独 apk 安装文件 ;


    插件化 中 , " 宿主 " 模块 和 " 插件 " 模块 可以分开进行编译 , 二者之间互不影响 , 各个模块可以并发进行开发 , " 宿主 " 模块 可以 动态更新插件 ,





    二、插件化示例



    如像 支付宝 等类似的特大型应用 , 内部提供了几百个小的应用模块 , 不可能在开发时就将其集成进去 , 这些软件的本体只有 100 MB 左右 , 不可能将所有的应用都纳入进去 ;

    分辨原生组件与 Web 组件 : 在大型应用中 , 有些应用是使用 WebView 嵌入前端小程序 , 有些是远程组件 , 在 " 开发者选项 " 中 , 打开 " 显示布局边界 " 选项 , 如果是 WebView 就只有一个框 , 如果是原生组件 , 对应的 TextView , ImageView 等都有各自的边界 , 借助布局边界显示可分辨出该第三方小程序是 前端 WebView 界面还是原生应用 ;

    打开 " 显示布局边界 " 选项 :

    在这里插入图片描述

    支付宝主界面效果 : 主界面是原生界面 ;

    在这里插入图片描述

    饿了么界面时 WebView 界面 , 主要内容是 前端小程序 开发的 ;

    在这里插入图片描述

    财富管理模块的股票模块 , 是原生应用 ;

    在这里插入图片描述

    支付宝的主体框架是 Android 原生应用 , 其中的第三方功能 , 小程序 , 基本都是 Web 前端页面 ;

    目前这类应用的开发趋势是使用 Web 应用 替换 原生应用 ;


    股票模块这类 原生应用 , 一般不会在支付宝开发时 , 打包在其中 , 而是通过 插件化 机制 , 动态部署其插件 apk ;

    这类软件第一次打开时 , 需要下载该模块的 插件 apk , 并安装 , 此时会卡顿以下 , 之后可以流畅访问 ;





    三、插件化标准引入



    插件化 中 , " 宿主 " 模块 和 " 插件 " 模块 可以分开进行编译 , 二者之间互不影响 , 各个模块可以并发进行开发 , " 宿主 " 模块 可以 动态更新插件 ;

    " 宿主 " 模块是当前运行的应用 , " 插件 " 模块是下载的插件模块编译后打包的 apk 文件 ;

    在不安装 插件 apk 的情况下 , 调用该 插件中的功能 , 如 Activity , Service , 代码逻辑等 ;


    不是任意 apk 文件都可以接入到 " 宿主 " 插件中 , 要接入的 apk 必须符合一定的标准 ;

    在 apk 插件没有安装 , 因此不存在上下文 , 调用插件中的 Activity 界面时 , 需要将上下文传给插件的 Activity ,

    展开全文
  • Android插件化原理(一)Activity插件化

    万次阅读 多人点赞 2018-06-04 22:45:38
    四大组件的插件化插件化技术的核心知识点,而Activity插件化更是重中之重,Activity插件化主要有三种实现方式,分别是反射实现、接口实现和Hook技术实现。反射实现会对性能有所影响,主流的插件化框架没有采用此...

    title: " Android插件化原理(一)Activity插件化"
    date: 2018-5-28 00:16
    photos:

    • https://s2.ax1x.com/2019/05/31/VlkKJA.jpg
      tag:
    • Android应用层
    • Android插件化原理
      categories:
    • Android应用层

    本文首发于微信公众号「后场村刘皇叔」

    关联系列

    Android深入四大组件系列
    Android解析AMS系列
    Android解析ClassLoader系列

    前言

    四大组件的插件化是插件化技术的核心知识点,而Activity插件化更是重中之重,Activity插件化主要有三种实现方式,分别是反射实现、接口实现和Hook技术实现。反射实现会对性能有所影响,主流的插件化框架没有采用此方式,关于接口实现可以阅读dynamic-load-apk的源码,这里不做介绍,目前Hook技术实现是主流,因此本篇文章主要介绍Hook技术实现。
    Hook技术实现主要有两种解决方案 ,一种是通过Hook IActivityManager来实现,另一种是Hook Instrumentation实现。在讲到这两个解决方案前,我们需要从整体上了解Activity的启动流程。

    1.Activity的启动过程回顾

    Activity的启动过程主要分为两种,一种是根Activity的启动过程,一种是普通Activity的启动过程。关于根Activity的启动过程在在介绍过,这里来简单回顾下,如下图所示。
    VlkMRI.png
    图 1
    首先Launcher进程向AMS请求创建根Activity,AMS会判断根Activity所需的应用程序进程是否存在并启动,如果不存在就会请求Zygote进程创建应用程序进程。应用程序进程启动后,AMS会请求应用程序进程创建并启动根Activity。
    普通Activity和根Activity的启动过程大同小异,但是没有这么复杂,因为不涉及应用程序进程的创建,跟Laucher也没关系,如下图所示。
    Vlkuid.png
    图2
    上图抽象的给出了普通Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建普通Activity(步骤1),AMS会对
    这个Activty的生命周期管和栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity(步骤2)。

    2.Hook IActivityManager方案实现

    AMS是在SystemServer进程中,我们无法直接进行修改,只能在应用程序进程中做文章。可以采用预先占坑的方式来解决没有在AndroidManifest.xml中显示声明的问题,具体做法就是在上图步骤1之前使用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验。
    接着在步骤2之后用插件Activity替换占坑的Activity,接下来根据这个解决方案我们来实践一下。

    2.1 注册Activity进行占坑

    为了更好的讲解启动插件Activity的原理,这里省略了插件Activity的加载逻辑,直接创建一个TargetActivity来代表已经加载进来的插件Activity,接着我们再创建一个SubActivity用来占坑。在AndroidManifest.xml中注册SubActivity,如下所示。
    AndroidManifest.xml

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.liuwangshu.pluginactivity">S
        <application
           ...
            <activity android:name=".StubActivity"/>
        </application>
    </manifest>
    

    TargetActivity用来代表已经加载进来的插件Activity,因此不需要在AndroidManifest.xml进行注册。如果我们直接在MainActivity中启动TargetActivity肯定会报错(android.content.ActivityNotFoundException异常)。

    2.2 使用占坑Activity通过AMS验证

    为了防止报错,需要将启动的TargetActivity替换为SubActivity,用SubActivity来通过AMS的验证。在第六章时讲过Android 8.0与7.0的AMS家族有一些差别,主要是Android 8.0去掉了AMS的代理ActivityManagerProxy,代替它的是IActivityManager,直接采用AIDL来进行进程间通信。
    Android7.0的Activity的启动会调用ActivityManagerNative的getDefault方法,如下所示。
    frameworks/base/core/java/android/app/ActivityManagerNative.java

     static public IActivityManager getDefault() {
            return gDefault.get();
        }
     private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
            protected IActivityManager create() {
                IBinder b = ServiceManager.getService("activity");
                if (false) {
                    Log.v("ActivityManager", "default service binder = " + b);
                }
                IActivityManager am = asInterface(b);
                if (false) {
                    Log.v("ActivityManager", "default service = " + am);
                }
                return am;
            }
        };
    

    getDefault方法返回了IActivityManager类型的对象,IActivityManager借助了Singleton类来实现单例,而且gDefault又是静态的,因此IActivityManager是一个比较好的Hook点。
    Android8.0的Activity的启动会调用ActivityManager的getService方法,如下所示。
    frameworks/base/core/java/android/app/ActivityManager.java

      public static IActivityManager getService() {
            return IActivityManagerSingleton.get();
        }
    
        private static final Singleton<IActivityManager> IActivityManagerSingleton =
                new Singleton<IActivityManager>() {
                    @Override
                    protected IActivityManager create() {
                        final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                        final IActivityManager am = IActivityManager.Stub.asInterface(b);
                        return am;
                    }
                };
    

    同样的,getService方法返回了了IActivityManager类型的对象,并且IActivityManager借助了Singleton类来实现单例,确定了无论是Android7.0还是Android8.0,IActivityManager都是比较好的Hook点。Singleton类如下所示,后面会用到。
    frameworks/base/core/java/android/util/Singleton.java

    public abstract class Singleton<T> {
        private T mInstance;
        protected abstract T create();
        public final T get() {
            synchronized (this) {
                if (mInstance == null) {
                    mInstance = create();
                }
                return mInstance;
            }
        }
    }
    

    由于Hook需要多次对字段进行反射操作,先写一个字段工具类FieldUtil:
    FieldUtil.java

    public class FieldUtil {
        public static Object getField(Class clazz, Object target, String name) throws Exception {
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            return field.get(target);
        }
        public static Field getField(Class clazz, String name) throws Exception{
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            return field;
        }
        public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            field.set(target, value);
        }
    

    其中setField方法不会马上用到,接着定义替换IActivityManager的代理类IActivityManagerProxy,如下所示。

    public class IActivityManagerProxy implements InvocationHandler {
        private Object mActivityManager;
        private static final String TAG = "IActivityManagerProxy";
        public IActivityManagerProxy(Object activityManager) {
            this.mActivityManager = activityManager;
        }
        @Override
        public Object invoke(Object o, Method method, Object[] args) throws Throwable {
            if ("startActivity".equals(method.getName())) {//1
                Intent intent = null;
                int index = 0;
                for (int i = 0; i < args.length; i++) {
                    if (args[i] instanceof Intent) {
                        index = i;
                        break;
                    }
                }
                intent = (Intent) args[index];
                Intent subIntent = new Intent();//2
                String packageName = "com.example.liuwangshu.pluginactivity";
                subIntent.setClassName(packageName,packageName+".StubActivity");//3
                subIntent.putExtra(HookHelper.TARGET_INTENT, intent);//4
                args[index] = subIntent;//5
            }
            return method.invoke(mActivityManager, args);
        }
    }
    

    Hook点IActivityManager是一个接口,建议采用动态代理。在注释1处拦截startActivity方法,接着获取参数args中第一个Intent对象,它是原本要启动插件TargetActivity的Intent。注释2、3处新建一个subIntent用来启动的StubActivity,注释4处将这个TargetActivity的Intent保存到subIntent中,便于以后还原TargetActivity。注释5处用subIntent赋值给参数args,这样启动的目标就变为了StubActivity,用来通过AMS的校验。
    最后用代理类IActivityManagerProxy来替换IActivityManager,如下所示。

    HookHelper.java

    public class HookHelper {
        public static final String TARGET_INTENT = "target_intent";
        public static void hookAMS() throws Exception {
            Object defaultSingleton=null;
            if (Build.VERSION.SDK_INT >= 26) {//1
                Class<?> activityManageClazz = Class.forName("android.app.ActivityManager");
                //获取activityManager中的IActivityManagerSingleton字段
                defaultSingleton=  FieldUtil.getField(activityManageClazz ,null,"IActivityManagerSingleton");
            } else {
                Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
                //获取ActivityManagerNative中的gDefault字段
                defaultSingleton=  FieldUtil.getField(activityManagerNativeClazz,null,"gDefault");
            }
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstanceField= FieldUtil.getField(singletonClazz ,"mInstance");//2
            //获取iActivityManager
            Object iActivityManager = mInstanceField.get(defaultSingleton);//3
            Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[] { iActivityManagerClazz }, new IActivityManagerProxy(iActivityManager));
            mInstanceField.set(defaultSingleton, proxy);
        }
    }
    

    首先在注释1处对系统版本进行区分,最终获取的是 Singleton<IActivityManager>类型的IActivityManagerSingleton或者gDefault字段。注释2处获取Singleton类中的mInstance字段,从前面Singleton类的代码可以得知mInstance字段的类型为T,在注释3处得到IActivityManagerSingleton或者gDefault字段中的T的类型,T的类型为IActivityManager。最后动态创建代理类IActivityManagerProxy,用IActivityManagerProxy来替换IActivityManager。
    自定义一个Application,在其中调用HookHelper 的hookAMS方法,如下所示。
    MyApplication.java

    public class MyApplication extends Application {
        @Override
        public void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            try {
                HookHelper.hookAMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    在MainActivity中启动TargetActivity,如下所示。
    MainActivity.java

    public class MainActivity extends Activity {
        private Button bt_hook;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            bt_hook = (Button) this.findViewById(R.id.bt_hook);
            bt_hook.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Intent intent = new Intent(MainActivity.this, TargetActivity.class);
                    startActivity(intent);
                }
            });
        }
    }
    

    点击Button时,启动的并不是TargetActivity而是SubActivity,同时Log中打印了"hook成功",说明我们已经成功用SubActivity通过了AMS的校验。

    2.3 还原插件Activity

    前面用占坑Activity通过了AMS的校验,但是我们要启动的是插件TargetActivity,还需要用插件TargetActivity来替换占坑的SubActivity,这一替换的时机就在图2的步骤2之后。
    ActivityThread启动Activity的过程,如图3所示。
    Vlk1QP.png

    图3

    ActivityThread会通过H将代码的逻辑切换到主线程中,H类是ActivityThread的内部类并继承自Handler,如下所示。
    frameworks/base/core/java/android/app/ActivityThread.java

    private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    public static final int PAUSE_ACTIVITY          = 101;
    ...
       public void handleMessage(Message msg) {
                if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
                switch (msg.what) {
                    case LAUNCH_ACTIVITY: {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                        final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    
                        r.packageInfo = getPackageInfoNoCheck(
                                r.activityInfo.applicationInfo, r.compatInfo);
                        handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    } break;
                    ...
                  }
    ...
    }
    

    H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。那么在哪进行替换呢?接着来看Handler的dispatchMessage方法:
    frameworks/base/core/java/android/os/Handler.java

     public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
    

    Handler的dispatchMessage用于处理消息,看到如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。因此,mCallback可以作为Hook点,我们可以用自定义的Callback来替换mCallback,自定义的Callback如下所示。
    HCallback.java

    public class HCallback implements Handler.Callback{
        public static final int LAUNCH_ACTIVITY = 100;
        Handler mHandler;
        public HCallback(Handler handler) {
            mHandler = handler;
        }
        @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实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动SubActivity的Intent替换为启动TargetActivity的Intent。接着我们在HookHelper中定义一个hookHandler方法如下所示。

    HookHelper.java

     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));
        }
    

    ActivityThread类中有一个静态变量sCurrentActivityThread,用于表示当前的ActivityThread对象,因此在注释1处获取ActivityThread中定义的sCurrentActivityThread对象。注释2处获取ActivityThread类的mH字段,接着在注释3处获取当前ActivityThread对象中的mH对象,最后用HCallback来替换mH中的mCallback。
    在MyApplication的attachBaseContext方法中调用HookHelper的hookHandler方法,运行程序,当我们点击启动插件按钮,发现启动的是插件TargetActivity。

    2.4 插件Activity的生命周期

    插件TargetActivity确实启动了,但是它有生命周期吗?这里从源码角度来进行分析,Activity的finish方法可以触发Activity的生命周期变化,和Activity的启动过程类似,finish方法如下所示。
    frameworks/base/core/java/android/app/Activity.java

     public void finish() {
            finish(DONT_FINISH_TASK_WITH_ACTIVITY);
        }
     private void finish(int finishTask) {
            if (mParent == null) {
                int resultCode;
                Intent resultData;
                synchronized (this) {
                    resultCode = mResultCode;
                    resultData = mResultData;
                }
                if (false) Log.v(TAG, "Finishing self: token=" + mToken);
                try {
                    if (resultData != null) {
                        resultData.prepareToLeaveProcess(this);
                    }
                    if (ActivityManager.getService()
                            .finishActivity(mToken, resultCode, resultData, finishTask)) {//1
                        mFinished = true;
                    }
                } catch (RemoteException e) {
                    // Empty
                }
            } else {
                mParent.finishFromChild(this);
            }
        }
    

    finish方法的调用链和Activity的启动过程是类似的,注释1处会调用的AMS的finishActivity方法,接着是AMS通过ApplicationThread调用ActivityThread,ActivityThread向H发送DESTROY_ACTIVITY类型的消息,H接收到这个消息会执行handleDestroyActivity方法,handleDestroyActivity方法又调用了performDestroyActivity方法,如下所示。
    frameworks/base/core/java/android/app/ActivityThread.java

      private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                int configChanges, boolean getNonConfigInstance) {
            ActivityClientRecord r = mActivities.get(token);//1
            ...
                try {
                    r.activity.mCalled = false;
                    mInstrumentation.callActivityOnDestroy(r.activity);//2
                   ...
                } catch (SuperNotCalledException e) {
                 ...
                }
            }
            mActivities.remove(token);
            StrictMode.decrementExpectedActivityCount(activityClass);
            return r;
       
    

    注释1处通过IBinder类型的token来获取ActivityClientRecord,ActivityClientRecord用于描述应用进程中的Activity。在注释2处调用Instrumentation的callActivityOnDestroy方法来调用Activity的OnDestroy方法,并传入了r.activity。前面的例子我们用SubActivity替换了TargetActivity通过了AMS的校验,这样AMS只知道SubActivity的存在,那么AMS是如何能控制TargetActivity生命周期的回调的?我们接着往下看,启动Activity时会调用ActivityThread的performLaunchActivity方法,如下所示。
    frameworks/base/core/java/android/app/ActivityThread.java

     private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
              ...
          
                java.lang.ClassLoader cl = appContext.getClassLoader();
                activity = mInstrumentation.newActivity(
                        cl, component.getClassName(), r.intent);//1
               ...
                 activity.attach(appContext, this, getInstrumentation(), r.token,
                            r.ident, app, r.intent, r.activityInfo, title, r.parent,
                            r.embeddedID, r.lastNonConfigurationInstances, config,
                            r.referrer, r.voiceInteractor, window, r.configCallback);
               ...
                mActivities.put(r.token, r);//2
               ...
            return activity;
        }
    

    注释1处根据Activity的类名用ClassLoader加载Acitivty,接着调用Activity的attach方法,将r.token赋值给Activity的成员变量mToken。在注释2处将ActivityClientRecord根据r.token存在mActivities中(mActivities类型为ArrayMap<IBinder, ActivityClientRecord> ),再结合Activity的finish方法的注释1处,可以得出结论:AMS和ActivityThread之间的通信采用了token来对Activity进行标识,并且此后的Activity的生命周期处理也是根据token来对Activity进行标识的。
    回到我们这个例子来,我们在Activity启动时用插件TargetActivity替换占坑SubActivity,这一过程在performLaunchActivity方法调用之前,因此注释2处的r.token指向的是TargetActivity,在performDestroyActivity的注释1处获取的就是代表TargetActivity的ActivityClientRecord,可见TargetActivity是具有生命周期的。

    3.Hook Instrumentation方案实现

    Hook Instrumentation实现要比Hook IActivityManager实现要简洁一些,示例代码会和Hook IActivityManager实现有重复,重复的部分这里不再赘述。
    Hook Instrumentation实现同样也需要用到占坑Activity,与Hook IActivityManager实现不同的是,用占坑Activity替换插件Activity以及还原插件Activity的地方不同。Acitivty的startActivity方法调用时序图如图4所示。
    VlkQzt.png
    图4
    从图4可以发现,在Activity通过AMS校验前,会调用Activity的startActivityForResult方法:
    frameworks/base/core/java/android/app/Activity.java

     public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
                @Nullable Bundle options) {
            if (mParent == null) {
                options = transferSpringboardActivityOptions(options);
                Instrumentation.ActivityResult ar =
                    mInstrumentation.execStartActivity(
                        this, mMainThread.getApplicationThread(), mToken, this,
                        intent, requestCode, options);
              ...
            } else {
             ...
            }
        }
    

    startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期。
    如图3所示,ActivityThread启动Activity的过程中会调用ActivityThread的performLaunchActivity方法,如下所示。
    frameworks/base/core/java/android/app/ActivityThread.java

     private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {       
            ...
            //创建要启动Activity的上下文环境
            ContextImpl appContext = createBaseContextForActivity(r);
            Activity activity = null;
            try {
                java.lang.ClassLoader cl = appContext.getClassLoader();
                //用类加载器来创建Activity的实例
                activity = mInstrumentation.newActivity(
                        cl, component.getClassName(), r.intent);//1
              ...
            } catch (Exception e) {
              ...
            }
          ...
            return activity;
        }
    

    注释1处调用了mInstrumentation的newActivity方法,其内部会用类加载器来创建Activity的实例。看到这里我们可以得到方案,就是在Instrumentation的execStartActivity方法中用占坑SubActivity来通过AMS的验证,在Instrumentation的newActivity方法中还原TargetActivity,这两部操作都和Instrumentation有关,因此我们可以用自定义的Instrumentation来替换掉mInstrumentation。首先我们自定义一个Instrumentation,在execStartActivity方法中将启动的TargetActivity替换为SubActivity,如下所示。
    InstrumentationProxy.java

    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;
        }
    }
    

    首先查找要启动的Activity是否已经在AndroidManifest.xml中注册了,如果没有就在注释1处将要启动的Activity(TargetActivity)的ClassName保存起来用于后面还原TargetActivity,接着在注释2处替换要启动的Activity为StubActivity,最后通过反射调用execStartActivity方法,这样就可以用StubActivity通过AMS的验证。在InstrumentationProxy 的newActivity方法还原TargetActivity,如下所示。
    InstrumentationProxy.java

    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);
    }
    

    newActivity方法中创建了此前保存的TargetActivity,完成了还原TargetActivity。
    编写hookInstrumentation方法,用InstrumentationProxy替换mInstrumentation:
    HookHelper.java

      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()));
        }
    

    注释1处获取ContextImpl类的ActivityThread类型的mMainThread字段,注释2出获取当前上下文环境的ActivityThread对象。
    注释3出获取ActivityThread类中的mInstrumentation字段,最后用InstrumentationProxy来替换mInstrumentation。
    在MyApplication的attachBaseContext方法中调用HookHelper的hookInstrumentation方法,运行程序,当我们点击启动插件按钮,发现启动的是插件TargetActivity。

    4. 总结

    这一节我们学到了启动插件Activity的原理,主要的方案就是先用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity替换占坑的Activity。为了更好的讲解启动插件Activity的原理,本小节省略了插件Activity的加载逻辑,直接创建一个TargetActivity来代表已经加载进来的插件Activity。同时这一节使我们更好的理解了Activity的启动过程。更多的Android插件化原理请查看《Android进阶解密》。


    这里不仅分享大前端、Android、Java等技术,还有程序员成长类文章。
    展开全文
  • 插件化篇 - 插件化框架对比

    千次阅读 2019-04-10 15:52:23
    来看看现有插件化框架的对比。 目录: MulitDex 引起的问题 插件化需要解决的问题与方案 插件化实现方案分析对比 1.MulitDex 引起的问题 在应用安装到手机上的时候 dex 文件的安装是复杂的,有可能会因为第...

    来看看现有插件化框架的对比。

    目录:

    1. MulitDex 引起的问题
    2. 插件化需要解决的问题与方案
    3. 插件化实现方案分析对比

     

     

    1. MulitDex 引起的问题

    在应用安装到手机上的时候 dex 文件的安装是复杂的,有可能会因为第二个 dex 文件太大导致 ANR。使用了 mulitDex 的 App 有可能在 4.0(api level 14) 以前的机器上无法启动,因为 Dalvik linearAlloc bug(Issue 22586) 。使用了 mulitDex 的 App 在 runtime期间有可能因为 Dalvik linearAlloc limit (Issue 78035) Crash。该内存分配限制在 4.0 版本被增大,但是 5.0 以下的机器上的 Apps 依然会存在这个限制。

    主 dex 被 dalvik 虚拟机执行时候,哪些类必须在主 dex 文件里面这个问题比较复杂。build tools 可以搞定这个问题。但是如果你代码存在反射和 native 的调用也不保证 100% 正确。

    对于 davilk 和 art 虚拟机 Mulitdex 的不同: ART 模式相比原来的 Dalvik,会在安装 APK 的时候,使用 Android 系统自带的dex2oat 工具把 APK 里面的 .dex 文件转化成 OAT 文件。

    这里说一下罗迪的快速加载 Mulitdex 方案:art 虚拟机对 dex 优化需要很长时间,加载插件 dex 跳过优化实现加速,跳过会影响类加载的性能。

     

     

    2. 插件化需要解决的问题与方案

     

    • 2.1 实现插件化需要解决的技术点
    • 问题 1:资源如何加载,资源冲突问题如何解决。
    • 问题 2:代码如何加载访问。
    • 问题 3:插件的管理后台包括的内容。
    • 问题 4:插件的增量更新问题。
    • 问题 5:加载插件中的 so 库。

     

    • 2.2 解决方案

    针对问题 1:

    原理:资源 id 是在编译时生成的,其生成的规则是 0xPPTTNNNN,PP 段是用来标记 apk 的,默认情况下系统资源 PP 是01,应用程序的 PP 是 07。TT 段,是用来标记资源类型的,比如图标、布局等,相同的类型 TT 值相同,但是同一个 TT 值不代表同一种资源,例如这次编译的时候可能使用 03 作为 layout 的 TT,那下次编译的时候可能会使用 06 作为TT的值,具体使用那个值,实际上和当前 APP 使用的资源类型的个数是相关联的。NNNN 则是某种资源类型的资源 id,默认从1 开始,依次累加。

    那么我们要解决资源 id 问题,就可从 TT 的值开始入手,只要将每次编译时的 TT 值固定,即可以让资源 id 达到分组的效果,从而避免重复。例如将宿主程序的 layout 资源的 TT 固定为 33,将插件程序资源的 layout 的 TT 值固定为 03 (可不对插件程序的资源 id 做任何处理,使其使用编译出来的原生的值),即可解决资源 id 重复的问题了。

    固定资源 id 的 TT 值的办法非常简单,提供一份 public.xml,在 public.xml 中指定什么资源类型以什么 TT 值开头即可。比如:

    public.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <public type="mipmap" name="ic_launcher" id="0x7f030001" />
        <public type="drawable" name="ic_notice" id="0x7f02002b" />
    
    
        <!--<public-padding type="drawable" name="abc_" start="0x7f120000" end="0x7f12ffff" />-->
    </resources>

    build.gradle:

    afterEvaluate {
        for (variant in android.applicationVariants) {
            def scope = variant.getVariantData().getScope()
            String mergeTaskName = scope.getMergeResourcesTask().name
            def mergeTask = tasks.getByName(mergeTaskName)
    
            mergeTask.doLast {
                copy {
                    int i=0
                    from(android.sourceSets.main.res.srcDirs) {
                        include 'values/public.xml'
                        rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml")
                    }
    
                    into(mergeTask.outputDir)
                }
            }
        }
    }

    这样就能使用 public.xml 固定资源 id,public.xml 用来告诉 Android 资源打包工具 aapt,将类型为 drawable 的资源 ic_notice 的id 固定为 0x7f02002b。注意,在开发应用程序时,一般是不需要用到 public.xml 文件的,因为我们的资源基本上都是在内部使用的,不会导出来给第三方应用程序使用。只在内部使用的资源,不管它的 id 如何变化,我们都可以通过 R.java 文件定义的常量来正确地引用它们。只有系统定义的资源包才会使用到 public.xml 文件,因为它定义的资源是需要提供给第三方应用程序使用的。当然还有做插件化的时候,处理资源冲突问题。

    还有一个方法是通过定制过的 aapt 在编译时指定插件的 PP 段的值来实现分组,重写过的 aapt 指定 PP 段来实现 id 分组。

    • 方案 1:将插件 apk 资源解压,通过操作文件的方式来使用,这个只是理论上可行,实际使用的时候还是有很多的问题。
    • 方案 2:重写 Context 的 getResource()、getAsset() 之类的方法,资源冲突需要扩展 aapt 实现。
    • 方案 3:打包后执行一个脚本修改资源 id。

     

    针对问题 2:

    原理说明:主要就是 classloader 加载 dex,代理模式就是本身宿主中有 Activity,通过欺骗系统来创建 Activity,欺骗系统的部分 hook 的有深有浅 (对比 DroidPlugin 和 Small),让这个 Activity 有生命周期,而动态加载模式就是运行时动态创建并编译一个 Activity 类,需要使用动态创建类的工具实现动态字节码操作。

    • 方案 1:简单加载模式。
    • 方案 2:Activity 代理模式。
    • 方案 3:动态加载模式。

     

    针对问题 3:

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

     

     

    3. 插件化实现方案分析对比

     

    • AndroidDynamicLoader

    AndroidDynamicLoader 的作者是大众点评的屠毅敏,应该是最早的动态加载实现方案了。作者在介绍这个框架时形容宿主 App就好像浏览器,但它加载的并不是网页,而是运行在 Android 系统上的插件。这个方案主要是在插件中使用 Fragment,在宿主中使用 Activity 去动态加载插件中的 Fragment。
    github 链接:https://github.com/mmin18/AndroidDynamicLoader

    • 23Code

    23Code 并不是一个开源的项目,它是一个完整的 apk。在应用市场可以下载得到,它内部展示了一些有趣的动效,通过下载的方式,可以直接运行起来,这就是插件化的真实案例。

    • Altas

    altas 是阿里的伯奎分享出来的一个插件化的方案。
    分享视频链接:http://v.youku.com/v_show/id_XNTMzMjYzMzM2.html
    对应的 PPT:http://club.alibabatech.org/resource_detail.htm?topicId=84

    这里的分享主要是将一些思路,业务方面的场景,解决的问题。后来 github 上有了对应的开源项目叫 OpenAltas,后改名ACDD。
    ACDD 的链接:https://github.com/bunnyblue/ACDD

    官方对这个库的说明是非代理 Android 动态热部署框架,而且在视频介绍中,伯奎也讲到,这个方案,就是把宿主当做一个容器,动态加载对应的插件。视频中也有讲到,要做到让插件无感知地运行在这个容器中,需要 hook 很多系统层面的东西,比如:ActivityThread,LoadedApk,ContextImpl,PackageParser,ActivityManagerNative 等,即当插件需要系统的服务来提供对应功能的时候,将这个行为拦截掉,宿主就可以在插件和 Android 系统中间做些手脚了。在当时,Altas 是最为先进插件化技术了,为淘宝客户端提供了很多加载其他业务插件的能力。

    • Dynamic-load-apk

    Dynamic-Load-Apk 简称 DL,这个开源框架作者是任玉刚,他的实现方式是,在宿主中埋一个代理 Activity,更改 ClassLoader后找到加载插件中的 Activity,使用宿主中的 Activity 作为代理,回调给插件中 Activity 所以对应的生命周期。这个思路与AndroidDynamicLoader 有点像,都是做一个代理,只不过 Dynamic-load-apk 加载的插件中的 Activity。
    项目地址:https://github.com/singwhatiwanna/dynamic-load-apk
    项目说明:http://blog.csdn.net/singwhatiwanna/article/details/40283117

    • DroidPlugin

    DroidPlugin 是张勇实现的一套插件化方案,它的原理也是 Hook 客户端一侧的系统 API,可能与 Altas 所 Hook 的点不同,细想应该是基本相同的。
    项目地址:https://github.com/DroidPluginTeam/DroidPlugin

    项目中 DOC 文件夹有很多关于项目的讲解说明。
    张勇本人对 DroidPlugin 的讲解:http://www.infoq.com/cn/presentations/the-realization-principle-and-application-of-droidplugin?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=presentations_link&utm_content=link_text
    维术对这个框架的讲解:http://weishu.me/2016/01/28/understand-plugin-framework-overview/

    维术的这几篇 blog 很详细滴讲解了 DroidPlugin 是如何实现四大组件的插件化的,还有几篇有规划但是没有写的方面,希望后面能补全。

    • VirtualApp

    VirtualApp 作者是高中生罗迪,据说这个 Android 大牛初三的时候就开始研究双开、插件化的技术,相当了不起。项目的思路与DroidPlugin 相似,不过他没有提供 Service 的代理,而是使用 ContentProvider 来代替 Service 在宿主中作为真正的运行体。
    项目地址:https://github.com/asLody/VirtualApp

    • DynamicAPK

    DynamicAPK 是携程推出的动态加载方案。
    项目说明:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading
    项目地址:https://github.com/CtripMobile/DynamicAPK

    与 ACDD 一样修改了aapt,使得插件与宿主的资源不会出现相同 id 的问题。

     

    整体对比如下:

     

     

     

     

    展开全文
  • 插件化篇 - 插件化技术实现原理

    千次阅读 2019-04-10 17:54:06
    插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 可以理解为插件。支持插件化的 app 可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一...

    插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 可以理解为插件。支持插件化的 app 可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现 app 功能的动态扩展。想要实现插件化,主要是解决下面三个问题:

    • 插件中代码的加载和与主工程的互相调用。
    • 插件中资源的加载和与主工程的互相访问。
    • 四大组件生命周期的管理。

     

     

    1. 插件化发展

    第一代:dynamic-load-apk 最早使用 ProxyActivity 这种静态代理技术,由 ProxyActivity 去控制插件中 PluginActivity 的生命周期。该种方式缺点明显,插件中的 activity 必须继承 PluginActivity,开发时要小心处理 context。而 DroidPlugin 通过 Hook 系统服务的方式启动插件中的 Activity,使得开发插件的过程和开发普通的 app 没有什么区别,但是由于 hook 过多系统服务,异常复杂且不够稳定。

    第二代:为了同时达到插件开发的低侵入性 (像开发普通 app 一样开发插件) 和框架的稳定性,在实现原理上都是趋近于选择尽量少的 hook,并通过在 manifest 中预埋一些组件实现对四大组件的插件化。另外各个框架根据其设计思想都做了不同程度的扩展,其中 Small 更是做成了一个跨平台,组件化的开发框架。

    第三代:VirtualApp 比较厉害,能够完全模拟 app 的运行环境,能够实现 app 的免安装运行和双开技术。Atlas 是阿里今年开源出来的一个结合组件化和热修复技术的一个 app 基础框架,其广泛的应用与阿里系的各个 app,其号称是一个容器化框架。

     

     

    2. 基本原理

     

    • 2.1 类加载

    外部 apk 中类的加载:

    Android 中常用的有两种类加载器,DexClassLoader 和 PathClassLoader,它们都继承于BaseDexClassLoader。

        public class DexClassLoader extends BaseDexClassLoader {
    
            public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
                super(dexPath, new File(optimizedDirectory), libraryPath, parent);
            }
        }
    
        public class PathClassLoader extends BaseDexClassLoader {
    
            public PathClassLoader(String dexPath, ClassLoader parent) {
                super(dexPath, null, null, parent);
            }
    
            public PathClassLoader(String dexPath, String libraryPath,
                ClassLoader parent) {
                super(dexPath, null, libraryPath, parent);
            }
        }

    区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。所以我们可以用DexClassLoader 去加载外部的 apk,用法如下:

    /**
         * 第一个参数为 apk 的文件目录
         * 第二个参数为内部存储目录
         * 第三个为库文件的存储目录
         * 第四个参数为父加载器
         */
        new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)

    双亲委托机制:

    ClassLoader 调用 loadClass 方法加载类:

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { 
           //首先从已经加载的类中查找
            Class<?> clazz = findLoadedClass(className);    
        if (clazz == null) {
                ClassNotFoundException suppressed = null;     
               try {   
                    //如果没有加载过,先调用父加载器的loadClass
                    clazz = parent.loadClass(className, false);
                } catch (ClassNotFoundException e) {
                    suppressed = e;
                }      
            if (clazz == null) {        
                    try {           
           
                      //父加载器都没有加载,则尝试加载
                        clazz = findClass(className);
                    } catch (ClassNotFoundException e) {
                        e.addSuppressed(suppressed);       
                         throw e;
                    }
                }
            }    
                return clazz;
        }

    可以看出 ClassLoader 加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的 findClass 方法加载,该机制很大程度上避免了类的重复加载。

    DexClassLoader 的 DexPathList:

    DexClassLoader 重载了 findClass 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造DexClassLoader 时生成的,其内部包含了 DexFile。DexPathList 的 loadClass 会去遍历 DexFile 直到找到需要加载的类:

        public Class findClass(String name, List<Throwable> suppressed) { 
           //循环dexElements,调用DexFile.loadClassBinaryName加载class
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;    
            if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);   
                         if (clazz != null) {   
                           return clazz;
                    }
                }
            }  
          if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }     
             return null;
        }

    有一种热修复技术正是利用了 DexClassLoader 的加载机制,将需要替换的类添加到 dexElements 的前面,这样系统会使用先找到的修复过的类。 

     

    • 2.2 单 DexClassLoader 与多 DexClassLoader

    通过给插件 apk 生成相应的 DexClassLoader 便可以访问其中的类,这边又有两种处理方式,有单 DexClassLoader 和多DexClassLoader 两种结构。

    多 DexClassLoader:

    对于每个插件都会生成一个 DexClassLoader,当加载该插件中的类时需要通过对应 DexClassLoader 加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。RePlugin 采用的是该方案。

    单 DexClassLoader:

    将插件的 DexClassLoader 中的 pathList 合并到主工程的 DexClassLoader 中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个 common 插件中直接供其他插件使用。Small 采用的是这种方式。

    互相调用

    插件调用主工程:

    • 在构造插件的 ClassLoader 时会传入主工程的 ClassLoader 作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。

    主工程调用插件:

    • 若使用多 ClassLoader 机制,主工程引用插件中类需要先通过插件的 ClassLoader 加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
    • 若使用单 ClassLoader 机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。

     

    • 2.3 资源加载

    Android 系统通过 Resource 对象加载资源,下面代码展示了该对象的生成过程:

    //创建AssetManager对象 
    AssetManager assets = new AssetManager();
     //将apk路径添加到AssetManager中
      if (assets.addAssetPath(resDir) == 0){              
        return null;  
    }
     //创建Resource对象
    
    r = new Resources(assets, metrics, getConfiguration(), compInfo);

    因此,只要将插件 apk 的路径加入到 AssetManager 中,便能够实现对插件资源的访问。具体实现时,由于 AssetManager 并不是一个 public 的类,需要通过反射去创建,并且部分 Rom 对创建的 Resource 类进行了修改,所以需要考虑不同 Rom 的兼容性。

    资源路径的处理:

    和代码加载相似,插件和主工程的资源关系也有两种处理方式。

    • 合并式:addAssetPath 时加入所有插件和主工程的路径。
    • 独立式:各个插件只添加自己 apk 路径。

    合并式由于 AssetManager 中加入了所有插件和主工程的路径,因此生成的 Resource 可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源 id 会存在相同的情况,在访问时会产生资源冲突。

    独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的 Resource 对象。

    Context 的处理:

    通常我们通过 Context 对象访问资源,光创建出 Resource 对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以 VirtualAPK 的处理方式为例:

    第一步:创建 Resource

    if (Constants.COMBINE_RESOURCES) {
        //插件和主工程资源合并时需要hook住主工程的资源
        Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
        ResourcesManager.hookResources(context, resources);  
          return resources;
    } else {  
          //插件资源独立,该resource只能访问插件自己的资源
        Resources hostResources = context.getResources();
        AssetManager assetManager = createAssetManager(context, apk);  
            return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }

    第二步:hook 主工程的 Resource 

    对于合并式的资源访问方式,需要替换主工程的 Resource,下面是具体替换的代码。

    public static void hookResources(Context base, Resources resources) { 
       try {
                ReflectUtil.setField(base.getClass(), base, "mResources", resources);
                Object loadedApk = ReflectUtil.getPackageInfo(base);
                ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);
    
                Object activityThread = ReflectUtil.getActivityThread(base);
                Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");       
         if (Build.VERSION.SDK_INT < 24) {
                    Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");
                    Object key = map.keySet().iterator().next();
                    map.put(key, new WeakReference<>(resources));
                } else {                // still hook Android N Resources, even though it's unnecessary, then nobody will be strange.
                    Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");
                    Object key = map.keySet().iterator().next();
                    Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");
                    map.put(key, new WeakReference<>(resourcesImpl));
                }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    注意下上述代码 hook 了几个地方,包括以下几个 hook 点:

    • 替换了主工程 context 中 LoadedApk 的 mResource 对象。
    • 将新的 Resource 添加到主工程 ActivityThread 的 mResourceManager 中,并且根据 Android 版本做了不同处理。

    第三步:关联 resource 和 Activity

    Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
    activity.setIntent(intent);
    //设置Activity的mResources属性,Activity中访问资源时都通过mResources
    
    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());

    上述代码是在 Activity 创建时被调用的 (后面会介绍如何 hook Activity 的创建过程),在 activity 被构造出来后,需要替换其中的mResources 为插件的 Resource。由于独立式时主工程的 Resource 不能访问插件的资源,所以如果不做替换,会产生资源访问错误。

    做完以上工作后,则可以在插件的 Activity 中放心的使用 setContentView,inflater 等方法加载布局了。

    资源冲突

    合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源 id 可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源 id。资源 id 是由 8 位 16 进制数表示,表示为 0xPPTTNNNN。PP 段用来区分包空间,默认只区分了应用资源和系统资源,TT 段为资源类型,NNNN 段在同一个 APK 中从 0000 递增。

     

    所以思路是修改资源 id 的 PP 段,对于不同的插件使用不同的 PP 段,从而区分不同插件的资源。具体实现方式有两种

    • 修改 aapt 源码,编译期修改 PP 段。
    • 修改 resources.arsc 文件,该文件列出了资源 id 到具体资源路径的映射。

    具体实现可以分别参考 Atlas 框架和 Small 框架。推荐第二种方式,不用入侵原有的编译流程。

     

     

    3. 四大组件支持

    Android 开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。如常用的四大组件,Activity,Service,BroadcastReceiver 和 ContentProvider。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以 Activity 最为复杂,不同框架采用的方法也不尽相同。下面以 Activity 为例详细介绍插件化如何支持组件生命周期的管理,大致分为两种方式:

    • ProxyActivity 代理。
    • 预埋 StubActivity,hook 系统启动 Activity 的过程。

     

    • 3.1 ProxyActivity 代理

    ProxyActivity 代理的方式最早是由 dynamic-load-apk 提出的,其思想很简单,在主工程中放一个 ProxyActivy,启动插件中的Activity 时会先启动 ProxyActivity,在 ProxyActivity 中创建插件 Activity,并同步生命周期。

    1. 首先需要通过统一的入口 (如 PluginManager) 启动插件 Activity,其内部会将启动的插件 Activity 信息保存下来,并将intent 替换为启动 ProxyActivity 的 intent。
    2. ProxyActivity 根据插件的信息拿到该插件的 ClassLoader 和 Resource,通过反射创建 PluginActivity 并调用其 onCreate 方法。
    3. PluginActivty 调用的 setContentView 被重写了,会去调用 ProxyActivty 的 setContentView。由于 ProxyActivity 重写了getResource 返回的是插件的 Resource,所以 setContentView 能够访问到插件中的资源。同样 findViewById 也是调用ProxyActivity 的。
    4. ProxyActivity 中的其他生命周期回调函数中调用相应 PluginActivity 的生命周期。

    代理方式的关键总结起来有下面两点:

    1. ProxyActivity 中需要重写 getResouces,getAssets,getClassLoader 方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如 onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged 等需要转发给插件。
    2. PluginActivity 中所有调用 context 的相关的方法,如 setContentView,getLayoutInflater,getSystemService 等都需要调用 ProxyActivity 的相应方法。

    该方式虽然能够很好的实现启动插件 Activity 的目的,但是由于开发式侵入性很强,dynamic-load-apk 之后的插件化方案很少继续使用该方式,而是通过 hook 系统启动 Activity 的过程,让启动插件中的 Activity 像启动主工程的 Activity 一样简单。

     

    • 3.2 hook 方式
    1. Activity1 调用 startActivity,实际会调用 Instrumentation 类的 execStartActivity 方法,Instrumentation 是系统用来监控Activity 运行的一个类,Activity 的整个生命周期都有它的影子。
    2. 通过跨进程的 binder 调用,进入到 ActivityManagerService 中,其内部会处理 Activity 栈。之后又通过跨进程调用进入到Activity2 所在的进程中。
    3. ApplicationThread 是一个 binder 对象,其运行在 binder 线程池中,内部包含一个 H 类,该类继承于类 Handler。ApplicationThread 将启动 Activity2 的信息通过 H 对象发送给主线程。
    4. 主线程拿到 Activity2 的信息后,调用 Instrumentation 类的 newActivity 方法,其内通过 ClassLoader 创建 Activity2 实例。

    下面介绍如何通过 hook 的方式启动插件中的 Activity,需要解决以下两个问题:

    • 插件中的 Activity 没有在 AndroidManifest 中注册,如何绕过检测。
    • 如何构造 Activity 实例,同步生命周期。

    解决方法有很多种,以 VirtualAPK 为例,核心思路如下:

    • 先在 Manifest 中预埋 StubActivity,启动时 hook 住,将 Intent 替换成 StubActivity。
    • 通过插件的 ClassLoader 反射创建插件 Activity。
    • 之后 Activity 的所有生命周期回调都会通知给插件 Activity。

    下面具体分析整个过程涉及到的代码:

    替换系统 Instrumentation

    VirtualAPK 在初始化时会调用 hookInstrumentationAndHandler,该方法 hook 了系统的 Instrumentaiton 类,由上文可知该类和Activity 的启动息息相关。

    private void hookInstrumentationAndHandler() { 
       try {  
             //获取Instrumentation对象
            Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);   
                  //构造自定义的VAInstrumentation
            final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation); 
                         //设置ActivityThread的mInstrumentation和mCallBack
            Object activityThread = ReflectUtil.getActivityThread(this.mContext);
            ReflectUtil.setInstrumentation(activityThread, instrumentation);
            ReflectUtil.setHandlerCallback(this.mContext, instrumentation); 
             this.mInstrumentation = instrumentation;
        } catch (Exception e) {
            e.printStackTrace();
        }
     }

    该段代码将主线程中的 Instrumentation 对象替换成了自定义的 VAInstrumentation 类。在启动和创建插件 activity 时,该类都会偷偷做一些手脚。

    hook activity 启动过程

    VAInstrumentation 类重写了 execStartActivity 方法:

    public ActivityResult execStartActivity(
        Intent intent) {
    //转换隐式intent
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); 
       if (intent.getComponent() != null) {  
          //替换intent中启动Activity为StubActivity
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }        
    
        //调用父类启动Activity的方法
    public void markIntentIfNeeded(Intent intent) { 
       if (intent.getComponent() == null) {  
             return;
        }
    
        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();    // search map and return specific launchmode stub activity
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

    execStartActivity 中会先去处理隐式 intent,如果该隐式 intent 匹配到了插件中的 Activity,将其转换成显式。之后通过markIntentIfNeeded 将待启动的的插件 Activity 替换成了预先在 AndroidManifest 中占坑的 StubActivity,并将插件 Activity 的信息保存到该 intent 中。其中有个 dispatchStubActivity 函数,会根据 Activity 的 launchMode 选择具体启动哪个 StubActivity。VirtualAPK 为了支持 Activity 的 launchMode 在主工程的 AndroidManifest 中对于每种启动模式的 Activity 都预埋了多个坑位。

    hook Activity 的创建过程

    上一步欺骗了系统,让系统以为自己启动的是一个正常的 Activity。当构建 Activity 时,再将插件的 Activity 换回来,此时调用的是 VAInstrumentation 类的 newActivity 方法。

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent){
        try {
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            //通过LoadedPlugin可以获取插件的ClassLoader和Resource
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                    //获取插件的主Activity
            String targetClassName = PluginUtil.getTargetActivity(intent);
                    if (targetClassName != null) { 
                       //传入插件的ClassLoader构造插件Activity
                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                activity.setIntent(intent);
                        //设置插件的Resource,从而可以支持插件中资源的访问
                try {
                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                } catch (Exception ignored) { 
                                       // ignored.
                }  
              return activity;
            }
        }    return mBase.newActivity(cl, className, intent);
    }

    由于 AndroidManifest 中预埋的 StubActivity 并没有具体的实现类,所以此时会发生 ClassNotFoundException。之后在处理异常时取出插件 Activity 的信息,通过插件的 ClassLoader 反射构造插件的 Activity。 

    一些额外操作

    插件 Activity 构造出来后,为了能够保证其正常运行还要做些额外的工作,VAInstrumentation 类做了一些处理:

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) { 
       final Intent intent = activity.getIntent();
           if (PluginUtil.isIntentFromPlugin(intent)) {
            Context base = activity.getBaseContext();
                   try {
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
                ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
                ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
                ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
                   // set screenOrientation
                ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent)); 
                  if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(activityInfo.screenOrientation);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        mBase.callActivityOnCreate(activity, icicle);
    }

    这段代码主要是将 Activity 中的 Resource,Context 等对象替换成了插件的相应对象,保证插件 Activity 在调用涉及到 Context的方法时能够正确运行。

    经过上述步骤后,便实现了插件 Activity 的启动,并且该插件 Activity 中并不需要什么额外的处理,和常规的 Activity 一样。那问题来了,之后的 onResume,onStop 等生命周期怎么办呢?答案是所有和 Activity 相关的生命周期函数,系统都会调用插件中的 Activity。原因在于 AMS 在处理 Activity 时,通过一个 token 表示具体 Activity 对象,而这个 token 正是和启动 Activity 时创建的对象对应的,而这个 Activity 被我们替换成了插件中的 Activity,所以之后 AMS 的所有调用都会传给插件中的 Activity。

    小结

    VirtualAPK 通过替换了系统的 Instrumentation,hook 了 Activity 的启动和创建,省去了手动管理插件 Activity 生命周期的繁琐,让插件 Activity 像正常的 Activity 一样被系统管理,并且插件 Activity 在开发时和常规一样,即能独立运行又能作为插件被主工程调用。

    其他插件框架在处理 Activity 时思想大都差不多,无非是这两种方式之一或者两者的结合。在 hook 时,不同的框架可能会选择不同的 hook 点。如 360 的 RePlugin 框架选择 hook 了系统的 ClassLoader,在判断出待启动的 Activity 是插件中的时,会调用插件的 ClassLoader 构造相应对象。另外 RePlugin 为了系统稳定性,选择了尽量少的 hook,因此它并没有选择 hook 系统的startActivity 方法来替换 intent,而是通过重写 Activity 的 startActivity,因此其插件 Activity 是需要继承一个类似 PluginActivity的基类的。不过 RePlugin 提供了一个 Gradle 插件将插件中的 Activity 的基类换成了 PluginActivity,用户在开发插件 Activity 时也是没有感知的。

     

    • 3.3 其他组件

    四大组件中 Activity 的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:

    • Service:Service 和 Activity 的差别在于,Activity 的生命周期是由用户交互决定的,而 Service 的生命周期是我们通过代码主动调用的,且 Service 实例和 manifest 中注册的是一一对应的。实现 Service 插件化的思路是通过在 manifest 中预埋StubService,hook 系统 startService 等调用替换启动的 Service,之后在 StubService 中创建插件 Service,并手动管理其生命周期。
    • BroadCastReceiver:解析插件的 manifest,将静态注册的广播转为动态注册。
    • ContentProvider:类似于 Service 的方式,对插件 ContentProvider 的所有调用都会通过一个在 manifest 中占坑的ContentProvider 分发。

     

     

     

     

    展开全文
  • android插件化

    千次阅读 2020-09-02 17:52:35
    插件化是一种动态升级app功能的解决方案,不同于热修复(仅仅是修复功能),类似于RN、Weex(目的类似)。都是为了在不发版本的情况下,可以让用户用上最新的功能。不过RN、Weex还额外支持跨平台。相对于RN和Weex,...
  • Android插件化浅析

    千次阅读 2016-07-03 13:21:13
    插件化是2016年移动端最火爆的几个名词之一,目前淘宝、百度、腾讯等都有成熟的动态加载框架,包括apkplug, 本篇博客就来探讨一下插件化设计。本博客主要从以下几个方面对插件化进行解析: Ø 为什么会提出插件化...
  • 1.什么是插件化开发 首先我们先来看看通过插件化开发后的APP是什么样的效果。这里就用最典型的插件化APP 360手机卫士 来演示。 可以看到,打开应用后在切换到工具箱中有很多功能,我的工具中先是有8个自带的功能...
  • Android模块化、组件化、插件化区别

    千次阅读 2019-04-23 10:55:39
    插件化(application)是所有组件都为apk的特殊组件化,特点可热更新 通讯方式不同点: 模块化相互引入,需要引入需要的module 组件化通讯方式分为隐式和路由。 插件化本身是不同进程,因此是binder机制进程间...
  • Android插件化方案实践

    千次阅读 2020-07-27 10:18:59
    插件化概述 1、插件化和组件化的区别 组件化是将一个app拆分为多个模块进行协作开发,每个模块都是一个单独的组件,这些组件可以相互依赖,也可以单独调试运行。但是最终发布的时候,这些组件会合并在一起,组成一...
  • Android组件化和插件化的概念

    千次阅读 多人点赞 2020-11-10 15:43:17
    一、什么是组件化和插件化 组件化开发 就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一...
  • 安卓插件(安卓应用插件化

    千次阅读 2019-03-15 14:23:33
    安卓应用插件化: 为了减小安卓应用单个包的大小,方便应用功能的动态更新、变动,需要对应用进行分解。 将一个功能众多的应用分解为: 一个主应用外壳 + n个功能插件 (1)主应用外壳提供给用户安装,在使用时可...
  • Android插件化

    千次阅读 2019-02-24 13:24:22
    Android插件化 ​ 减少安装包大小,实现app功能模块化动态扩展 发展历程 需解决三个问题 插件中代码的加载和与主工程的互相调用 插件中资源的加载和与主工程的互相访问 四大组件生命周期的管理 框架发展的三代 第一...
  • APP插件化/组件化框架分析

    千次阅读 2017-11-22 21:16:58
    VirtualAPK插件框架简单使用如感觉排版看着不舒服,可移步【APP插件化/组件化框架分析】。最近一段时间在研究插件化和组件化实现方案,今天也算整理一下笔记记录一下,记得之前讲述过一篇关于组件化的文章【Android ...
  • Android组件化和插件化开发

    千次阅读 2016-07-01 11:26:46
    Android组件化和插件化开发什么是组件化和插件化?组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将...
  • Android插件化、组件化总结

    千次阅读 2019-02-24 13:41:02
    架构设计之插件化、组件化 组件化方案 1、module library 切换 2、组件间跳转uri跳转 3、组件间通讯 binder机制 主工程(壳工程mudele) ​ 不分配任何具体业务逻辑。用于使用组合业务组件、初始化配置和发布应用配置...
  • 深入浅出Service插件化原理

    万次阅读 2019-01-29 09:11:32
    Service插件化的重点是保证它的优先级,需要一个真正的Service来实现,当启动插件Service时,就会先启动代理Service,当这个代理Service运行起来后,在它的onStartCommand等方法里面进行分发,执行插件Service的...
  • 插件化Activity

    万次阅读 2016-09-13 17:33:37
    我们为什么要学习插件化Activity呢?学习又有什么用呢?使用方向: SDK或者library我们都知道Activity对象是由系统创建产生的,所以我们必须在AndroidManifest.xml文件下引入该Activity,从而实现Activity的创建.这时...
  • 插件化技术:宿主访问插件资源

    千次阅读 2017-02-07 12:40:07
    本文同步自wing的地方酒馆最近在搞插件化,16年很火的东西,我又拖了1年才来研究,哈哈哈,正确下一个热门技术能提前一些吧。今天想跟大家讨论一下我在研究插件化过程中,遇到的一个容易混淆的点,那就是资源访问。...
  • java插件化开发

    千次阅读 2018-11-12 15:09:27
    我开始想一想什么是java常用的插件化开发、在我的印象中jpa等算是一种吧,那么接下来我们将开始总结一下java开发有哪些插件。 一、Small 一个小框架,将应用程序分成小部分 地址: http://code.wequick.net/Small...
  • 组件化,插件化和热更新

    千次阅读 2017-08-11 14:20:18
    组件化,插件化和热更新什么是组件化:
  • 闲话不多说,下面开始扯正题,最近有个同事问我“模块化、组件化,插件化还有热更新他们之间有什么关系和区别?“ 概述 随着产品的业务不断的增加,我们的APP中代码就会越来越多,这时侯为了方便我们多个成员之间...
  • Android插件化开发之OpenAtlas初体验 Android插件化开发之OpenAtlas生成插件信息列表 Android插件化开发之OpenAtlas资源打包工具补丁aapt的编译 Android插件化开发之OpenAtlas插件适配 Android插件化开发之解决...
  • C++插件化开发,插件化系统

    千次阅读 2016-05-21 00:00:58
    1.为什么需要插件化系统  “编程就是构建一个一个自己的小积木, 然后用自己的小积木搭建大系统”。  但是程序还是会比积木要复杂, 我们的系统必须要保证小积木能搭建出大的系统(必须能被组合),有必须能使...
  • 插件化框架集成-360插件框架DroidPlug

    千次阅读 2017-11-06 17:27:34
    一:使用插件化集成框架的好处:  1:打造轻量级APP,提高用户转化率  2:打破Dex65536的限制,妈妈再也不用担心我的编程  3:热修复,动态修复错误 二:既然插件化框架有这么多好处,那么为什么不适用呢,...
  • 微店 Android 插件化实践

    千次阅读 2017-06-14 15:11:50
    微店从 2016 年 4 月份开始进行插件化改造,到年底基本完成。现在一共有 29 个模块以插件化的方式运行,其中既有如商品、订单等的业务模块,也有像 Network、Cache 等的基础模块,已经很好地支持了微店多 Feature ...
  • 各种化,模块化、组件化、插件化、热更新、热修复…… 模块化:项目按照独立的模块进行划分 组件化:将项目按照单一的组件来进行划分结构 模块化与组件化的差别在于颗粒度,个人认为模块相对于组件的粒度较大!组件...
  • 目录 前言 一、模块化 二、组件化 1、概念 ...三、插件化 ...谈到热修复相信大家应该比较熟悉,因为它是...插件化和热修复同出一门,俩者都属于动态更新,而模块化和组件化是基础。相信看完本篇的内容,对于这些模糊...
  • Android插件化认知

    千次阅读 2016-12-29 15:58:04
    Android 插件化学习一突然想到Android 的插件化开发,于是网上搜罗资料,初步认知demo如下: 主要思想:利用 类加载器ClassLoader实现。解决主要问题:一个是65K 大小问题,另外可以动态加载apk实现程序的动态更新...
  • Android组件化和插件化开发简单介绍

    千次阅读 2019-06-03 19:52:59
    Android组件化和插件化开发简单介绍什么是组件化和插件化?为什么要项目组件化组件化插件化 什么是组件化和插件化? 组件化开发: 就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们...
  • android插件化之路

    千次阅读 2016-10-20 20:11:28
    在说插件化之前,我们的理解三个概念:插件化,组件化,动态加载(又叫热加载)Android 插件化 —— 指将一个程序划分为不同的部分,比如一般 App 的皮肤样式就可以看成一个插件 Android 组件化 —— 这个概念实际...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 651,134
精华内容 260,453
关键字:

插件化