[Android]朝花夕拾之使用DexClassLoader動態加載廣點通jar包

1. 背景

幾年前作過一個需求,背景是這樣的:android

  • 公司的App中接入了廣點通廣告,這須要在APP內嵌入廣告SDK。
  • 在OPPO VIVO等應用市場中,若是APP嵌入了第三方廣告SDK,會在應用詳情頁提示「有廣告」等字樣,影響用戶下載意願,甚至有些應用市場直接禁止上架。
  • 要實現的效果:既保證應用內能夠正常處理廣點通廣告邏輯,又要避開應用市場對APP內廣告SDK的檢查。

彼時採用的方案是對廣點通的jar包進行動態加載,即APP內不嵌入廣告SDK,在APP打開後,從服務器下載SDK到本地,使用類加載器去加載本地的SDK,經過反射機制建立廣告對象實例,完成加載須要。 當時作這些操做是爲了完成需求,並無對相關邏輯進行擴展,前段時間在梳理插件化框架時,發現當時用的不少思想和插件化的思想不謀而合,如今想來,若是繼續在原有基礎上擴展,添加插件化的思路,可能適用性會更高一些。如今將以前的思路作個記錄,後續有機會能夠繼續完善。服務器

2. 思路

  1. 經過DexClassLoader對廣點通jar包進行加載
  2. 在類加載器加載完成所需類後,使用反射機制建立廣告對象
  3. 使用動態代理代理廣點通的廣告回調方法
  4. 使用反射機制獲取加載廣告的方法
  5. 使用反射機制,利用第2步建立的廣告對象和第四部獲取的廣告加載方法對廣告進行加載

3. 實現

3.1 使用DexClassLoader加載jar包

String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
        String jarPath = sdcard + "/ecoomi/jar/gdt_dex.jar";
        File file = new File(jarPath);
        if (!file.exists()) {
            Log.d("zyl", "file not exist");
            return false;
        }
        String tmpPath = context.getApplicationContext().getDir("Jar", 0).getAbsolutePath();
        DexClassLoader cl = new DexClassLoader(jarPath, tmpPath
                , tmpPath, context.getClassLoader());
複製代碼

3.2 加載相關類並根據加載出的類對象建立廣告對象實例

廣點通廣告的構造方法有五個參數,分別是:markdown

  • Context 對象
  • adSize對象
  • appkey,String類型
  • appSecret, String類型
  • 廣告回調的監聽

這五個參數中,adSize和廣告回調監聽都是廣點通SDK提供,adSize使用反射建立對象,廣告回調監聽使用動態代理替換以前的接口app

try {
    // 廣告類
    Class<?> gdtClass = cl.loadClass("com.qq.e.ads.nativ.NativeExpressAD");
    // adSize類
    Class<?> adSizeClass = cl.loadClass("com.qq.e.ads.nativ.ADSize");
    // 廣告回調監聽類
    Class<?> listenerClass = cl.loadClass("com.qq.e.ads.nativ.NativeExpressAD$NativeExpressADListener");
    //AdSize的構造方法
    Constructor<?> adSizeConstructor = adSizeClass.getConstructor(int.class, int.class);

    //InvokeHander,入參是向app提供回調的自定義callBack
    GdtInvokeHandler handler = new GdtInvokeHandler(adCallBack);
    // 建立廣告回調監聽的動態代理
    Object listener = Proxy.newProxyInstance(cl, new Class[] { listenerClass }, handler);

    Log.d("zyl", "listener = " + listener.getClass().getName());
    // 獲取廣告類的構造方法
    Constructor<?> gdtConstructor = gdtClass.getConstructor(Context.class, adSizeClass, String.class, String.class, listenerClass);
    // 根據構造方法建立廣告對象實例
    gdtObject = gdtConstructor.newInstance(context,  adSizeConstructor.newInstance(-1, -2), APP_KEY, APP_KEY, listener);

    //todo 增長視頻廣告支持

    // 獲取加載廣告的方法
    loadAdMethod = gdtClass.getDeclaredMethod("loadAD", int.class);
    loadAdMethod.setAccessible(true);

    // 替換系統的ClassLoader爲自定義的
    loadApkClassLoader(context, cl);
    //加載廣點通所需的activity和Service(這裏應該不須要)
    cl.loadClass("com.qq.e.comm.DownloadService");
    cl.loadClass("com.qq.e.ads.ADActivity");
    cl.loadClass("com.qq.e.ads.PortraitADActivity");
    cl.loadClass("com.qq.e.ads.LandscapeADActivity");

    Log.d("zyl", "init success");
    return true;
} catch (IllegalArgumentException e) {
    Log.d("zyl", "IllegalArgumentException = " + e.getMessage());
} catch (IllegalAccessException e) {
    Log.d("zyl", "IllegalArgumentException = " + e.getMessage());
} catch (InvocationTargetException e) {
    Log.d("zyl", "IllegalArgumentException = " + e.getMessage());
} catch (Exception e) {
    Log.d("zyl", "GdtException = " + e.getMessage());
}
複製代碼

GdtInvokeHandler的實現:框架

private AdCallBack mAdCallback;

    public GdtInvokeHandler(AdCallBack adCallBack) {
        mAdCallback = adCallBack;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        Log.d("zyl", "InvokeHandler = " + method.getName());
        if ("onADLoaded".equals(method.getName())) {
            if (args != null && args.length > 0) {
                List list = (List) args[0];
                if (list != null && list.size() > 0) {
                    List<AdInterface> tempList = new ArrayList<>();
                    for (int i = 0; i < list.size(); i++) {
                        Object object = list.get(i);
                        GdtModelView view = new GdtModelView(object);
                        view.render();
                        tempList.add(view);
                    }
                    mAdCallback.onAdLoaded(tempList);
                }
            }
        } else if ("onRenderFail".equals(method.getName())) {

        } else if ("onRenderSuccess".equals(method.getName())) {

        } else if ("onADExposure".equals(method.getName())) {

        } else if ("onADClicked".equals(method.getName())) {

        } else if ("onADClosed".equals(method.getName())) {

        } else if ("onADLeftApplication".equals(method.getName())) {

        } else if ("onADOpenOverlay".equals(method.getName())) {

        } else if ("onADCloseOverlay".equals(method.getName())) {

        } else if ("onNoAD".equals(method.getName())) {
            Object object = args[0];

            Class clazz = object.getClass();
            try {
                Method errorMethod = clazz.getDeclaredMethod("getErrorMsg");
                errorMethod.setAccessible(true);
                String errorMessage = (String)errorMethod.invoke(object);
                Log.d("zyl", "errorMessage = " + errorMessage);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

        }
        return null;
    }
複製代碼

主要是講廣告回調的監聽全都轉到InvokeHandler中並在此處理相關邏輯ide

廣點通廣告須要打開SDK中定義的Activity和Service,若是用自定義的類加載器去加載,它只是加載了Activity和Service相關類,沒有通過AMS的處理,是不可能有生命週期方法、window、view等的建立的,怎麼辦呢,咱們須要讓系統的類加載器能夠加載jar包所在路徑的類,這樣在啓動Activity時,它會有相關處理,變成一個正常的Actiivity/Service。這個邏輯的處理在loadApkClassLoader方法:spa

private static void loadApkClassLoader(Context context, DexClassLoader dLoader){
        try{
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});//獲取主線程對象
            String packageName = context.getPackageName();//當前apk的包名
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                    wr.get(), dLoader);
            Log.i("zyl", "classloader:"+dLoader);
        }catch(Exception e){
            Log.i("zyl", "load apk classloader error:"+Log.getStackTraceString(e));
        }
    }
複製代碼

這時候就有同窗問了,你把這個類加載器替換了,那麼它不就只能加載jar包路徑下的類了麼,那我App中的類豈不是加載不了了?放心,不會有這個問題,看下類加載器的建立:插件

DexClassLoader cl = new DexClassLoader(jarPath, tmpPath
                , tmpPath, context.getClassLoader());
複製代碼

最後一個參數parent,它所傳的實參context.getClassLoader(),和LoadedApk中的是同一個,也就是說,咱們定義了一個增強版類加載器,它既能夠加載主APK所在路徑的類,也能夠加載jar包所在路徑的類線程

3.3 加載廣告:

public void loadGdtAd() {
        if (!inited) {
            inited = init(mContext, mAdCallBack);
            if (!inited) {
                return;
            }
        }
        try {
            loadAdMethod.invoke(gdtObject, new Object[] {10});
        } catch (IllegalAccessException e) {
            Log.d("zyl", "loadGdtAd  IllegalAccessException = " + e.getMessage());
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            Log.d("zyl", "loadGdtAd  InvocationTargetException = " + e.getMessage());
        } catch (Exception e) {
            Log.d("zyl", "loadGdtAd  Exception = " + e.getMessage());
        }
    }
複製代碼
相關文章
相關標籤/搜索