效能優化 (八) APK 加固之動態替換 Application

DevYK發表於2019-06-04

效能優化系列

APP 啟動優化

UI 繪製優化

記憶體優化

圖片壓縮

長圖優化

電量優化

Dex 加解密

動態替換 Application

APP 穩定性之熱修復原理探索

APP 持續執行之程式保活實現

ProGuard 對程式碼和資源壓縮

APK 極限壓縮

簡介

上一篇講了 dex 加密解密 還沒有看過的可以先去了解下 dex 怎麼加解密,這篇就來帶大家完成剩下的工作,dex 解密完成之後需要把代理 ProxyApplication 給刪除掉,然後把我們自己的 Application 給新增到我們程式中。想要替換 ProxyApplication 可不是一件簡單的事兒,首先必須的對 Application 啟動原始碼很熟悉才能對它進行操作,下面由我來帶著大家一起進入原始碼的世界吧。

Application 繫結過程

APP 啟動流程可以看我另外一篇文章效能優化(一)啟動優化,今天主要從 ActivityThread => main() 開始,下面以一個流程圖來說明一下:

效能優化 (八) APK 加固之動態替換 Application

XML 中如何解析我們的 Application

  • ActivityThread.java

    mian() -> thread.attach() -> attachApplication() -> 接收 AMS 發過來的引數之後 sendMessage(H.BIND_APPLICATION)-> 處理 BIND_APPLICATION -> handleBindApplication() 在這裡準備好 application - > Application app = data.info.makeApplication() - > mInitialApplication = app;

  • LoadedApk.java

    這個類就是 APK 在記憶體中的表示,可以得到如程式碼,資料,功能清單等資訊

    1. 通過 mApplicationInfo.className 得到我們註冊的全類名
    2. app = mActivityThread.mInstrumentation.newApplication () 建立 application
    3. 接下來會使用 appContext.setOuterContext(app)
    4. mApplication = app

反射需要替換的內容

  • ContextImpl -> mOuterContext(app) 通過 Application 的 attachBaseContext 回撥引數獲取
  • ActivityThread -> mAllApplication(arrayList) 通過 ContextImpl 的 mMainThread 屬性獲取
  • LoadedApk -> mApplication 通過 ContextImpl 的 mPackageInfo 屬性獲取

反射開始替換 Application

    boolean isBindReal;
    Application delegate;
    private void bindRealApplicatin() throws Exception {
        if (isBindReal) {
            return;
        }
        if (TextUtils.isEmpty(app_name)) {
            return;
        }
        //得到attachBaseContext(context) 傳入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //建立使用者真實的application (MyApplication)
        Class<?> delegateClass = Class.forName(app_name);
        delegate = (Application) delegateClass.newInstance();
        //得到attach()方法
        Method attach = Application.class.getDeclaredMethod("attach", Context.class);
        attach.setAccessible(true);
        attach.invoke(delegate, baseContext);


//        ContextImpl---->mOuterContext(app)   通過Application的attachBaseContext回撥引數獲取
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //獲取mOuterContext屬性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        mOuterContextField.set(baseContext, delegate);

//        ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread屬性
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        Object mMainThread = mMainThreadField.get(baseContext);

//        ActivityThread--->>mInitialApplication
        Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread,delegate);
//        ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread屬性
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        mAllApplications.remove(this);
        mAllApplications.add(delegate);

//        LoadedApk------->mApplication                      ContextImpl的mPackageInfo屬性
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo=mPackageInfoField.get(baseContext);

        Class<?> loadedApkClass=Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo,delegate);

        //修改ApplicationInfo className   LooadedApk
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        ApplicationInfo mApplicationInfo = (ApplicationInfo)mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className=app_name;

        delegate.onCreate();
        isBindReal = true;
    }
複製程式碼

現在重新簽名打包完成,啟動我們的 APK 看下 Log

效能優化 (八) APK 加固之動態替換 Application

2019-06-04 23:17:30.892 6064-6064/com.yk.dexdeapplication I/DevYK: provider onCreate:com.example.proxy_core.ProxyApplication@1ec3c70
2019-06-04 23:17:30.892 6064-6064/com.yk.dexdeapplication I/DevYK: provider onCreate:com.example.proxy_core.ProxyApplication@1ec3c70
2019-06-04 23:17:30.892 6064-6064/com.yk.dexdeapplication I/DevYK: provider onCreate:com.example.proxy_core.ProxyApplication
2019-06-04 23:17:30.895 6064-6064/com.yk.dexdeapplication I/DevYK: MyApplication onCreate()
2019-06-04 23:17:30.995 6064-6064/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App@300b5f6
2019-06-04 23:17:30.995 6064-6064/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App@300b5f6
2019-06-04 23:17:30.995 6064-6064/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App
2019-06-04 23:17:31.001 6064-6064/com.yk.dexdeapplication I/DevYK: provider delete:com.example.proxy_core.ProxyApplication@1ec3c70
2019-06-04 23:17:31.021 6064-6064/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App@300b5f6
2019-06-04 23:17:31.021 6064-6064/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App@300b5f6
2019-06-04 23:17:31.021 6064-6064/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App
2019-06-04 23:17:31.022 6064-6064/com.yk.dexdeapplication I/DevYK: reciver:android.app.ReceiverRestrictedContext@9b92293
2019-06-04 23:17:31.022 6064-6064/com.yk.dexdeapplication I/DevYK: reciver:com.yk.dexdeapplication.App@300b5f6
2019-06-04 23:17:31.022 6064-6064/com.yk.dexdeapplication I/DevYK: reciver:com.yk.dexdeapplication.App
複製程式碼

注意看 LOG

MyApplication onCreate()
複製程式碼

這裡已經替換成我們自己的 MyApplication , 而且 Activity 和 Service 獲取上下文也已經是我們替換成功的 Applicaton。但是...也許有的眼神比較好的已經看出問題了,為什麼內容提供者 Context 還是代理的 Application 而且比我們自己的應用還要先執行,那麼我們帶著這個問題去看 Application onCreate 之前做了什麼事兒。

效能優化 (八) APK 加固之動態替換 Application

我們點選 installlContentProviders(app,providers);

注意這裡傳進去的還是 代理 Context

效能優化 (八) APK 加固之動態替換 Application

重點在最後

效能優化 (八) APK 加固之動態替換 Application

注意看我 勾畫 的圈裡面的邏輯判斷,判斷當前應用的包名是否跟 XML 中的包名一致,如果一致我們就賦值,再次提醒下 這裡的 context 是我們代理的 context ,那麼我們怎麼做勒,我們在代理中重寫 PackageName 只要都不等 那麼就會走 else 會根據包名建立一個 Context

    /**
     * 讓程式碼走入if中的第三段中
     * @return
     */
    @Override
    public String getPackageName() {
        if(!TextUtils.isEmpty(app_name)){
            return "";
        }
        return super.getPackageName();
    }

    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
       if(TextUtils.isEmpty(app_name)){
           return super.createPackageContext(packageName, flags);
       }
        try {
            bindRealApplicatin();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return delegate;

    }
複製程式碼
  1. 首先判斷我們自己 XML 中的 app_name 是否為空
  2. 如果不為空,我們傳入一個 空包
  3. SDK 會判斷是否跟 XML 中的 pck 一樣,最後走 else 我們在重寫 createPackageContext 傳入我們自己應用的包名。會生成一個 Context

最後我們來驗證一下:

效能優化 (八) APK 加固之動態替換 Application

2019-06-05 00:12:30.271 7570-7570/com.yk.dexdeapplication I/DevYK: MyApplication onCreate()
2019-06-05 00:12:30.273 7570-7570/com.yk.dexdeapplication I/DevYK: provider onCreate:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.273 7570-7570/com.yk.dexdeapplication I/DevYK: provider onCreate:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.273 7570-7570/com.yk.dexdeapplication I/DevYK: provider onCreate:com.yk.dexdeapplication.App
2019-06-05 00:12:30.381 7570-7570/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.381 7570-7570/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.381 7570-7570/com.yk.dexdeapplication I/DevYK: activity:com.yk.dexdeapplication.App
2019-06-05 00:12:30.387 7570-7570/com.yk.dexdeapplication I/DevYK: provider delete:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.406 7570-7570/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.406 7570-7570/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.406 7570-7570/com.yk.dexdeapplication I/DevYK: service:com.yk.dexdeapplication.App
2019-06-05 00:12:30.408 7570-7570/com.yk.dexdeapplication I/DevYK: reciver:android.app.ReceiverRestrictedContext@b7a3b82
2019-06-05 00:12:30.408 7570-7570/com.yk.dexdeapplication I/DevYK: reciver:com.yk.dexdeapplication.App@1ec3c70
2019-06-05 00:12:30.408 7570-7570/com.yk.dexdeapplication I/DevYK: reciver:com.yk.dexdeapplication.App
複製程式碼

日誌中除了 BroadCase Context 是系統的以外,所有的 Context 都是我們替換的 Application Context。完美解決。不過這裡有一個隱藏 BUG ,據說面試題會問那麼是什麼勒?

可以在廣播中使用 context 在開啟一個廣播或者繫結一個服務嗎?

我們其實可以帶著這個問題看下原始碼

H -> RECEIVER 訊息

效能優化 (八) APK 加固之動態替換 Application

效能優化 (八) APK 加固之動態替換 Application

果然註冊廣播和繫結服務會拋一個異常。

總結

效能優化 (八) APK 加固之動態替換 Application

到這裡我們的加固已經講完了,從 dex 分包 -> 加密 -> 對齊 > 簽名 - > 打包壓縮成 APK 。一套完整的流程和程式碼都已經寫完了。跟市面上的加固流程原理都幾乎一樣。懂了原理再去使用第三方就輕車熟路了。

程式碼傳送

相關文章