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接口對象,然後調用方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章