android插件化开发指南-读书笔记(1)

读后感:

以前公司也做过插件化的开发,偶然的一天网上逛书店,看到这本书,买来看看,到现在大概看了几章,感觉这本书差点意思。包含的东西很多,但是感觉里面的东西都不是太深,甚至有些地方个人感觉都是错误的。比如里面contentprovider的本质是把数据存储到数据库里。当然也有很多以前没有接触过的,也是有所收获的,同时也感谢作者的分享。主观感觉,不喜勿喷。欢迎指正。

笔记

1.插件化的昨天

2012年7月27日,是Android插件化技术的第一个 里程碑。大众点评的屠毅敏(Github名为mmin18), 发布了第一个Android插件化开源项目 AndroidDynamicLoader

2013年,出现了23Code。23Code提供了一个 壳,在这个壳里可以动态下载插件,然后动态运行。 我们可以在壳外编写各种各样的控件,在这个框架下 运行。
2013年3月27日,第16期阿里技术沙龙,淘宝客 户端的伯奎做了一个技术分享,专门讲淘宝的Atlas插 件化框架,包括ActivityThread那几个类的Hook、增 量更新、降级、兼容等技术。这个视频[2],
2014年3月30日8点20分,是Android插件化的第 二个里程碑。任玉刚开源了一个Android插件化项目 dynamic-load-apk

2014年5月 张涛发布了他的第一个插件化框架 CJFrameForAndroid
2014年11月,houkx在GitHub上发布了插件化项 目android-pluginmgr

2015年。高中生Lody此刻还是高二学 生。他是从初中开始研究Android系统源码的。 第一个著名的开源项目是TurboDex
2015年3月底,Lody发布插件化项目Direct-Loadapk

2015年5月,limpoxe发布插件化框架AndroidPlugin-Framework[10]。
2015年7月,kaedea发布插件化框架androiddynamical-loading[11]。
2015年8月27日,是Android插件化技术的第三个 里程碑。张勇的DroidPlugin
2015年10月携程开源了他们的插件化框架 DynamicAPK[13],
2015年12月底,林光亮的Small框架发布
2016年8月,掌阅推出Zeus[14]。
2017年3月,阿里推出Atlas[15]。
2017年6月26日,360手机卫士的RePlugin[16]。
2017年6月29日,滴滴推出VisualApk[17]。

[1] 开源项目地址: https://github.com/mmin18/AndroidDynamicLoader
[2] 视频地址: http://v.youku.com/v_show/id_XNTMzMjYzMzM2.html
[3] 开源项目地址: https://github.com/singwhatiwanna/dynamic-load-apk
[4] 参考文章: https://blog.csdn.net/lostinai/article/details/50496976 47
[5] 张涛的开源实验室:https://kymjs.com
[6] 开源项目地址: https://github.com/kymjs/CJFrameForAndroid
[7] 开源项目地址:https://github.com/houkx/androidpluginmgr
[8] 开源项目地址: https://github.com/asLody/TurboDex
[9] 开源项目地址: http://git.oschina.net/oycocean/Direct-Load-apk
[10] 开源项目地址: https://github.com/limpoxe/Android-Plugin-Framework
[11] 开源项目地址:https://github.com/kaedea/androiddynamical-loading
[12] 田维术的技术博客:http://weishu.me
[13] 开源项目地址: https://github.com/CtripMobile/DynamicAPK
[14] 开源项目地址: https://github.com/iReaderAndroid/ZeusPlugin
[15] 开源项目地址:https://github.com/alibaba/atlas
[16] 开源项目地址: https://github.com/Qihoo360/RePlugin
[17] 开源项目地址: https://github.com/didi/VirtualAPK

2.android底层知识

  • binder原理,binder目的是解决跨进程通信。分为client和server两个进程。
  • AIDL 原理
  • AMS activityManagerService管理四大组件。
  • app startActivity启动流程 https://blog.csdn.net/chentaishan/article/details/105180793
  • ActivityThread
  • Context
  • service工作原理
  • broadcastReceiver工作原理 按照发送方式分三类:无序广播、有序广播、粘性广播
  • contentProvider工作原理
  • PMS PackageManagerService 获取apk包的信息。apk是一个zip压缩包,在文件头会记录压缩包的大小。apk在安装的时候都是解析apk中resource.arsc文件,这个文件存储资源的所有信息,包括在apk中的地址、大小。
  • PackParser 系统重启,会重新安装所有得app,这个由PMS完成。PackParser主要用来解析清单文件来获取四大组件信息。PackageParser中有一个方法,接受一个apkFile的参数,可以是当前apk,也可以是外部apk。所有通过这个类,来读取外部apk的信息。
  • ClassLoader 类加载器 有几个重要的类,子类BaseDexClassLoader,还有两个类似于孙子类,PathClassLoader DexClassLoader 构造器里有个optimizedDirectory参数用来加载dex文件的,且创建一个DexFile对象。
  • MultiDex 主要是android5.0之前的版本开始出现 65535问题,整个程序的方法栈只能最多为65535个。后来谷歌退出MultiDex工具来解决该问题。把一个dex拆分成多个dex。

3.反射

  • getClass() 得到是一个class对象。eg:User.class
  • Class.forName(); 通过包名获取class对象。

4.代理模式

  • 动态代理
    静态代理太不灵活,一个对象对应一个代理,代理类就会很多。这时候就产生了动态代理Proxy.newproxyInstance(ClassLoader classLoader,Class<?>[] interfaces,InvocationHandler h )
  • ActivityManagerNative的Hook
  • PMS Hook

5.对startActivity方法进行Hook

  • 创建一个baseActivity 重写startActivityForResult,进行拦截
  • 对Activity的Instrumentation 的方法 execStartActivity关键点进行Hook
    Activity.class 针对 mInstrumentation字段的execStartActivity进行hook
 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);
          
    }

创建mInstrumentation 的子类

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public EvilInstrumentation(Instrumentation base) {
        mBase = base;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        Log.d(TAG, "XXX到此一游!");

        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        Class[] p1 = {Context.class, IBinder.class,
                IBinder.class, Activity.class,
                Intent.class, int.class, Bundle.class};
        Object[] v1 = {who, contextThread, token, target,
                intent, requestCode, options};
        return (ActivityResult) RefInvoke.invokeInstanceMethod(
                mBase, "execStartActivity", p1, v1);
    }
}

通过反射调用

 Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(Activity.class, this, "mInstrumentation");
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

        RefInvoke.setFieldObject(Activity.class, this, "mInstrumentation", evilInstrumentation);


// 执行正常的页面跳转startActivity()
  • AMN的getDefault方法进行hook
    在Instrumentation的execStartActivity方法进行hook,对ActivitymanagerNative.getDefault()方法,通过动态代理的形式获取getDefault()方法返回IActivitiyManger接口。
public class AMSHookHelper {
    public static final String EXTRA_TARGET_INTENT = "extra_target_intent";

    public static void hookAMN() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {

        //获取AMN的gDefault单例gDefault,gDefault是final静态的
        Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");

        // gDefault是一个 android.util.Singleton<T>对象; 我们取出这个单例里面的mInstance字段
        Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", gDefault, "mInstance");

        // 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
        Class<?> classB2Interface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[] { classB2Interface },
                new MockClass1(mInstance));

        //把gDefault的mInstance字段,修改为proxy
        RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance", proxy);
    }
}

class MockClass1 implements InvocationHandler {

    private static final String TAG = "MockClass1";

    Object mBase;

    public MockClass1(Object base) {
        mBase = base;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if ("startActivity".equals(method.getName())) {

            Log.e("bao", method.getName());

            return method.invoke(mBase, args);
        }

        return method.invoke(mBase, args);
    }
}

在初始化hook逻辑

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        try {
            AMSHookHelper.hookAMN();
        } catch (Throwable throwable) {
            throw new RuntimeException("hook failed", throwable);
        }
    }

  • 对H类的mCallback 字段进行hook
    ActivityThread里H类可以进行通信,也就是启动页面。通过hook code=100 启动页面。依然使用动态代理去做。
public class HookHelper {

    public static void attachBaseContext() throws Exception {

        // 先获取到当前的ActivityThread对象
        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
        Handler mH = (Handler) RefInvoke.getFieldObject(currentActivityThread, "mH");

        //把Handler的mCallback字段,替换为new MockClass2(mH)
        RefInvoke.setFieldObject(Handler.class, mH, "mCallback", new MockClass2(mH));
    }
}


public class MockClass2 implements Handler.Callback {

    Handler mBase;

    public MockClass2(Handler base) {
        mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {

        switch (msg.what) {
            // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
            // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
            case 100:
                handleLaunchActivity(msg);
                break;
        }

        mBase.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        // 这里简单起见,直接取出TargetActivity;

        Object obj = msg.obj;

        Log.d("baobao", obj.toString());
    }
}


	初始化
   @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            // 在这里进行Hook
            HookHelper.attachBaseContext();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 再次对 Instrumentation 的字段进行Hook
    拦截ActivityThread类里Instrumentation字段,拦截它的newActivity方法和callActivityOnCreate方法
public class HookHelper {

    public static void attachContext() throws Exception{
        // 先获取到当前的ActivityThread对象
        Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");

        // 拿到原始的 mInstrumentation字段
        Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, "mInstrumentation");

        // 创建代理对象
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

        // 偷梁换柱
        RefInvoke.setFieldObject(currentActivityThread, "mInstrumentation", evilInstrumentation);
    }
}

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public EvilInstrumentation(Instrumentation base) {
        mBase = base;
    }

    public Activity newActivity(ClassLoader cl, String className,
                                Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {

        Log.d(TAG, "包建强到此一游!");

        return mBase.newActivity(cl, className, intent);
    }

    public void callActivityOnCreate(Activity activity, Bundle bundle) {

        Log.d(TAG, "到此一游!");

        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        Class[] p1 = {Activity.class, Bundle.class};
        Object[] v1 = {activity, bundle};
        RefInvoke.invokeInstanceMethod(
                mBase, "callActivityOnCreate", p1, v1);
    }
}


  • 对ActivityThread的Instrumentation字段进行hook
    此处使用context.startActvity进行启动页面,进行hook,和上述方式类型。

启动没有声明的activity

AMS的逻辑涉及整个系统,所以无法hook,也不能hook。
所以hook就从AMS的入口和出口进行hook。
1.创建一个StubActivity 且注册
2.封装数据到intent里,由stubActivity携带,且可以通过验证。等页面要启动的时候,把携带的数据提取出来,把页面替换。

6.插件化技术基础知识

  • 加载外部dex
    1.服务器下载到本地,然后去加载apk里dex(也可以放到hostapp里的assets,程序运行,在复制到内部系统中,再读取dex)
 /**
     * 把Assets里面得文件复制到 /data/data/files 目录下
     *
     * @param context
     * @param sourceName
     */
    public static void extractAssets(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeSilently(is);
            closeSilently(fos);
        }

    }

2.读取dex 生成对应的classloader

 File extractFile = this.getFileStreamPath(apkName);
        dexpath = extractFile.getPath();

        fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE

        classLoader = new DexClassLoader(dexpath,
                fileRelease.getAbsolutePath(), null, getClassLoader());

3.通过classloader的loadclass方法去加载dex中任何一个类。

try {
                    mLoadClassDynamic = classLoader.loadClass("com.example.plugin1.Dynamic");
                    Object dynamicObject = mLoadClassDynamic.newInstance();

                    IDynamic dynamic = (IDynamic) dynamicObject;
                    String content = dynamic.getStringForResId(MainActivity.this);
                    tv.setText(content);
                    Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show();
                } catch (Exception e) {
                    Log.e("DEMO", "msg:" + e.getMessage());
                }

provided 代替complie,好处是编译的时候用到对应的jar包,打包成apk并不会在apk中存在。provided 只支持jar包。

  • application 插件化解决方法 ,通过反射获取执行,但是缺点就是没有生命周期了。

7.资源初探

7.1资源分类

res下可编译的资源文件
assets目录下存放的原始资源文件

获取assets目录下所有文件

AssetManager assets = getResources().getAssets();
 final String[] list = assets.list("");

7.2 Resources AssetManager

  • AssetsManager 是获取assets文件夹的文件的管理器对象,addAssetsPath(path) 可以传入插件的apk路径。
  • Resources 获取各种资源的核心对象。
  • resources.arsc文件,apk打包产生的文件。其实是个hash表,存放每个十六进制值和资源的关系。

7.3 获取资源的方案

1.通过反射创建AssetManager对象,调用addAssetPath方法,把插件的路径添加到AssetManager对象中,这个AssetManager只为Plugin服务

    protected void loadResources() {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexpath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }

        mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

2.重写Activity的getAsset,getResource getTheme方法
3.加载外部插件,生成该插件的classLoader对象。

 		File extractFile = this.getFileStreamPath(apkName);
        dexpath = extractFile.getPath();

        fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE

        classLoader = new DexClassLoader(dexpath,fileRelease.getAbsolutePath(), null, getClassLoader());

4.通过反射,拿到插件中的类,构造处插件类的对象Dynamic接口对象,然后调用方法。

发布了118 篇原创文章 · 获赞 16 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/chentaishan/article/details/104954292
今日推荐