一、Android插件化技术

       我们在平时的开发过程中,会经常遇到产品需求的变更或者出现bug,在传统的模式中,我们需要首先需要修改代码,然后重新打包Apk,再交给公司的运营去官网或者应用商店上线,用户在打开应用的时候就会进行更新了。但是这种模式有几个缺点,一是上线周期长,从修改代码到用户更新需要较长的时间;二是用户更新代价较大,每次用户更新都需要下载整个Apk包,整个Apk包包括了一个应用的所有代码,要消耗用户较多的流量,并且,如果是一些重要的更新,为了确保用户都能更新到,还需要用到强制更新,即用户打开App后如果不更新应用则退出应用,这种对用户来说是极其不友好的。还有另外一种情况,某些较大的App功能很多,比如支付宝、微信等,如果将这些功能全部塞到一个Apk中,那将会是一个巨型Apk,用户在安装或者更新Apk时将会经过漫长的等待时间。基于以上两点,Android的插件化技术应运而生,插件化技术即将Apk按照功能模块划分,不同的功能打包成不同的Apk,然后应用的主Apk按需加载对应功能的Apk,用户只需要安装应用的主Apk即可,主Apk相当于一个壳,它会按需加载其他功能模块的Apk。通过这种模式,不仅解决了巨型Apk的问题,而且当某个功能模块需要变化时,也只需要修改对应功能的代码,打包功能Apk并更新即可,这样不仅可以让用户及时更新,而且更新的代价也很小。但是,我们知道,在Android中,没有安装的apk是不能直接运行的,那么要想实现插件化,我们就必须能够让主Apk能够加载功能Apk并运行。在这篇文章中,我们就一步步的分析如何实现Android的插件化。

二、需求分析

需求:实现在一个Apk中加载另外一个Apk并运行。

一般来说,Apk只有安装了才能够运行,Android在安装Apk时会解析Apk包,解析其中的AndroidManifest.xml文件,我们的四大组件(除了动态注册的广播)等都配置在其中,解析过程是由PackageManagerService完成的,PMS在解析Apk完成后,会将声明的四大组件的信息都注册在其中,并且应用在获取Apk资源时,也是需要通过PMS的协助完成,我们的应用可以通过上下文环境Context来和PMS打交道,对于一个没有安装过的Apk,其配置信息是没有在PMS中注册的,那么通过宿主Apk的上下文环境是无法去获取功能Apk中的四大组件以及资源等信息的,也就无法运行功能Apk。我们知道,PMS和AMS都运行在System Server进程中,我们的宿主Apk是运行在自己的应用进程中,不同的进程直接的数据是隔离的,我们无法在自己的应用进程中直接操作到运行在System Server进程中的PMS和AMS,两者之间的通信过程是通过Binder机制来完成的,也就是说,要想在宿主Apk中运行功能Apk的四大组件,就需要欺骗运行在System Server中的PMS和AMS,让其误以为我们的宿主Apk运行的是自己应用中的组件。基于以上思想,我们可以想到以下思路,首先来看一下针对Activity的处理:

1、在宿主Apk中注册一个代理的Activity,暂定为ProxyActivity;

2、当我们在宿主应用中要加载功能Apk时,首先要解析功能Apk,将其加载到对应的ClassLoader中;

3、启动ProxyActivity,因为ProxyActivity是在宿主Apk中注册过的,所有可以启动;

4、从功能Apk的ClassLoader中找到要功能Activity类;

5、通过反射创建功能Activity实例;

6、在ProxyActivity的所有生命周期回调函数中都调用功能Activity对应的回调函数,这样功能Activity要完成的功能就都在ProxyActivity中完成了。

可以看到,这是一种典型的代理模式,通过启动ProxyActivity来欺骗AMS,让AMS认为我们要启动的Activity是宿主Apk中的Activity,然而实际上ProxyActivity只是一个壳,它的主要作用是用来回调功能Activity的生命周期函数,这样就通过ProxyActivity完成了功能Activity的功能。前面说到过,没有安装过的Apk是没有在PMS中注册的,我们通过在宿主应用的Context是无法访问到功能Activity的资源的,这里我们暂时不讨论资源的加载问题,先只看如何通过ProxyActivity运行到功能Activity的代码。

如何动态加载Apk

首先,我们要知道如何将功能Apk加载到对应的ClassLoader中,关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader,看一下它们的对比:

DexClassLoader :可以加载文件系统上的jar、dex、apk

PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk

URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类

关于jar、dex和apk,dex和apk是可以直接加载的,因为它们都是或者内部有dex文件,而原始的jar是不行的,必须转换成Android虚拟机所能识别的字节码文件,通过以上介绍,我们需要用DexClassLoader来加载功能Apk。

代码

首先我们在宿主应用中新建ProxyActivity,代码如下:

package com.liunian.androidbasic.dynamicload;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;

public class ProxyActivity extends Activity {
    public static final String PLUGIN_DEX_PATH = "plugin.dex.path";
    public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";
    Activity mPluginActivity = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        if (intent != null) {
            // 从Intent中获得要启动的功能Apk的路径和Activity完整类名
            String pluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);
            String pluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);

            if (TextUtils.isEmpty(pluginDexPath) || TextUtils.isEmpty(pluginActivityClassName)) {
                return;
            }

            // 根据apk路径加载apk代码到DexClassLoader中
            File dexOutputDir = this.getDir("dex", 0);
            DexClassLoader dexClassLoader = new DexClassLoader(pluginDexPath,
                    dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
            if (dexClassLoader == null) {
                return;
            }

            // 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例
            Class pluginActivityClass = null;
            try {
                pluginActivityClass = dexClassLoader.loadClass(pluginActivityClassName);
                mPluginActivity = (Activity) pluginActivityClass.newInstance();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }

            // 调用功能Activity的setProxyActivity方法,给其设置代理Activity
            try {
                Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);
                setProxyActivityMethod.invoke(mPluginActivity, this);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        // 调用功能Activity实例的onCreate方法
        try {
            Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);
            onCreateMethod.setAccessible(true);
            onCreateMethod.invoke(mPluginActivity, savedInstanceState);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

宿主应用中启动功能Apk中的Activity:

Intent intent = new Intent();
        intent.putExtra(ProxyActivity.PLUGIN_DEX_PATH, "/sdcard/pluginmodule-debug.apk");
        intent.putExtra(ProxyActivity.PLUGIN_ACTIIVTY_CLASS_NAME, "com.alipay.pluginmodule.MainActivity");
        intent.setClass(MainActivity.this, ProxyActivity.class); // 其实启动的还是ProxyActivity

在Android studio中新建一个应用工程,MainActivity内容如下:

package com.alipay.pluginmodule;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {
    Activity mProxyActivity = null;

    public void setProxyActivity(Activity proxyActivity) {
        mProxyActivity = proxyActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i("liunianprint:", "MainActivity onCreate");
        mProxyActivity.setContentView(R.layout.activity_main);
    }
}

编译功能Apk,名称为pluginmodule-debug.apk,将其push到手机的sdcard目录下,在宿主应用中启动它,发现"MainActivity onCreate"有打印:

02-14 15:18:33.944 22742-22742/com.alipay.pluginmodule I/liunianprint:: MainActivity onCreate

但是界面上确没有显示任何内容,我们再MainActivity中通过setContentView给页面设置的布局并没有显示,为什么会这样呢?原因是因为我们通过宿主应用的上下文环境无法加载到在功能Apk中的资源,所以setContentView中设置的布局也就无效了,那么我们让功能Activity显示内容呢?既然无法通过设置布局id的方式给功能Activity添加布局,我们何不尝试一下手动给功能Activity添加控件呢?

public class MainActivity extends Activity {
    Activity mProxyActivity = null;

    public void setProxyActivity(Activity proxyActivity) {
        mProxyActivity = proxyActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i("liunianprint:", "MainActivity onCreate");
        mProxyActivity.setContentView(generateLayout()); // 给代理Activity设置手动生成的控件布局
    }

    private View generateLayout() {
        TextView textView = new TextView(mProxyActivity);
        textView.setText("我是Plugin的MainActivity");
        return textView;
    }
}

这个时候在从宿主Apk中启动功能Apk中的Activity,显示如下:

插件技术架构图 插件化技术_android

可以看到,成功的在宿主Apk中显示了功能Apk的内容,万里长征终于迈出了第一步,总结一下使用到的技术:

1、通过DexClassLoader动态的加载了功能Apk;

2、通过反射我们可以创建功能Activity的实例,并调用其函数;

3、通过注册代理Activity,然后在代理Activity的生命周期函数中调用功能Activity的生命周期函数,以达到让代理Activity实现功能Activity的效果;

4、功能Activity相当于只是一个代码块的封装,而代理Activity是实际启动的Activity,代理Activity可以看做一个壳,它会调用功能Activity的方法来完成功能Activity的效果。

加载Apk中的资源

在上面的代码中,我们通过动态加载技术成功的执行了功能Activity的代码,但是,却不能使用Apk中的资源,原因是因为通过宿主Apk的Context无法访问到功能Apk中的资源,那么,有没有什么办法可以让宿主Apk访问到功能Apk中的资源呢?下面我们就一起探寻一下解决办法。

Android的资源加载过程是一个复杂的过程,后面我们专门写一篇文章来讨论,这里,我们只探究一下如何通过Android系统提供的Api达到加载其他Apk中的资源的目的,首先,我们来看一下Activity的setContentView方法,看其是如何加载布局资源的:

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID); // 其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindow
        initWindowDecorActionBar();
    }

其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindow,继续看PhoneWindow的setContentView方法:

@Override
    public void setContentView(int layoutResID) {
        ...
        mLayoutInflater.inflate(layoutResID, mContentParent); // 调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上
        ...
    }

调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上,mContentParent是PhoneWindow中的根View---DecorView下的一个子控件,这样我们就通过setContentView方法将布局添加到了DecorView的子控件中。我们再来看一下LayoutInflater的inflate方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources(); // 根据上下文环境获得Resources对象

        final XmlResourceParser parser = res.getLayout(resource); // 根据Resources对象获得对应的布局文件
        try {
            return inflate(parser, root, attachToRoot); // 解析布局文件并生成控件树,如有必要,还需要将其添加到root中
        } finally {
            parser.close();
        }
    }

可以看到,Android获得资源首先是根据上下文环境获得一个Resources对象,然后在根据这个Resources对象即可获得其中的资源。那么我们如何构造一个加载了未安装Apk的Resource对象呢?我们来看一下Resources类的构造函数:

@Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

其中,AssetManager 表示资源管理器,Resources内部就是通过它来读取Apk中的资源的,DisplayMetrics表示屏幕分比率,Configuration表示设备配置。我们可以通过调用AssetManager的addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,那么通过Resources去查找资源时,就会查找Apk中的资源。我们可以通过反射来构建一个AssetMananger对象,然后调用其addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,再通过AssetMananger、DisplayMetrics以及Configuration来构建一个Resources对象,这样就可以访问到Resources中的资源了。代码如下:

public class ProxyActivity extends Activity {
    public static final String PLUGIN_DEX_PATH = "plugin.dex.path";
    public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";
    Activity mPluginActivity = null;
    Resources mPluginResourcs = null;
    String mPluginDexPath;
    String mPluginActivityClassName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        if (intent != null) {
            // 从Intent中获得要启动的功能Apk的路径和Activity完整类名
            mPluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);
            mPluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);

            if (TextUtils.isEmpty(mPluginDexPath) || TextUtils.isEmpty(mPluginActivityClassName)) {
                return;
            }

            loadApkResources(); // 加载资源

            // 根据apk路径加载apk代码到DexClassLoader中
            File dexOutputDir = this.getDir("dex", 0);
            DexClassLoader dexClassLoader = new DexClassLoader(mPluginDexPath,
                    dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
            if (dexClassLoader == null) {
                return;
            }

            // 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例
            Class pluginActivityClass = null;
            try {
                pluginActivityClass = dexClassLoader.loadClass(mPluginActivityClassName);
                mPluginActivity = (Activity) pluginActivityClass.newInstance();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }

            // 调用功能Activity的setProxyActivity方法,给其设置代理Activity
            try {
                Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);
                setProxyActivityMethod.invoke(mPluginActivity, this);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        // 调用功能Activity实例的onCreate方法
        try {
            Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);
            onCreateMethod.setAccessible(true);
            onCreateMethod.invoke(mPluginActivity, savedInstanceState);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    // 加载插件Apk的资源
    private void loadApkResources() {
        try {
            AssetManager assetManager = AssetManager.class.newInstance(); // 通过反射创建一个AssetManager对象
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); // 获得AssetManager对象的addAssetPath方法
            addAssetPathMethod.invoke(assetManager, mPluginDexPath); // 调用AssetManager的addAssetPath方法,将apk的资源添加到AssetManager中管理
            mPluginResourcs = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration()); // 根据AssetMananger创建一个Resources对象
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    // 重写ProxyActivity的getResources方法,让其返回插件Apk的资源对象
    @Override
    public Resources getResources() {
        if (mPluginResourcs != null) {
            return mPluginResourcs;
        }
        return super.getResources();
    }
}

同时,修改插件Apk的代码:

public class MainActivity extends Activity {
    Activity mProxyActivity = null;

    public void setProxyActivity(Activity proxyActivity) {
        mProxyActivity = proxyActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i("liunianprint:", "MainActivity onCreate");
        mProxyActivity.setContentView(R.layout.activity_main);
    }

}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="I am plugin's MainActivity!"
        android:layout_centerInParent="true"/>

</RelativeLayout>

重新打包插件Apk和宿主Apk,运行程序:

插件技术架构图 插件化技术_插件技术架构图_02

哈哈,我们成功的显示了插件Apk中的布局文件,总结一下使用到的技术:

1、通过反射构造一个资源管理AssetManager对象;

2、通过反射调用AssetManager的addAssetPath方法,将插件Apk中的资源添加到AssetManager对象中管理;

3、通过AssetManager对象、屏幕分辨率以及设置配置信息构造一个Resources对象;

4、重写ProxyActivity的getResources方法,让其返回可以访问插件Apk资源的Resources对象。

三、总结

这篇文章通过代理的方式实现简单的插件化,插件化主要要做到三件事:

1、加载未安装的Apk中的代码;

2、加载未安装的Apk中的资源;

3、欺骗AMS,绕过检测。

其中欺骗AMS,现在一般是使用Hook的方式实现的,Hook的方式需要我们对Activity的启动流程比较了解,可以Hook Ams的代理或者Hook Instrumentation,即欺骗AMS要启动的Activity是已经注册过的Activity,绕过检测,但是在创建Activity时在将其替换成插件中的Activity,这种方式我们在后面专门写一篇文章分析。