Android程式設計師必會技能—執行時動態生成類—之動態代理

DK_BurNIng發表於2019-03-04

談到java中的動態生成一個類,主要分為兩種方法,一種就是動態代理,另外一種就是asm。今天我們就來把對第一種方法
也就是動態代理生成類,這個流程搞清楚吃透。

要搞清楚動態代理,首先要弄明白為什麼需要動態代理?靜態代理不夠用嗎?

首先考慮一個場景,團隊中git提交的時候是不是都要經過leader review程式碼 同意以後 才會真正的上傳到master分支上?

那針對此場景,我們可以寫如下程式碼:

首先定義一下團隊成員介面

/**
 * Created by admin on 2018/12/8.
 */
public interface TeamMember {
    public void reviewCode();

}

複製程式碼

然後定義一個A小組的成員

/**
 * Created by admin on 2018/12/8.
 */
public class TeamAMember implements TeamMember {
    public TeamAMember(String name) {
        this.name = name;
    }

    String name;

    @Override
    public void reviewCode() {
        System.out.println("A小組成員" + name + "程式碼review過了");
    }
}


複製程式碼

然後定義一下我們的代理類,也就是leader來代理review大家的程式碼

/**
 * Created by admin on 2018/12/8.
 */
public class TeamLeaderProxy implements TeamMember {
    public TeamLeaderProxy(TeamMember teamMember) {
        //只有a組的成員程式碼 我才review程式碼
        if (teamMember instanceof TeamAMember) {
            this.teamMember = teamMember;
        }
    }

    TeamMember teamMember;


    @Override
    public void reviewCode() {
        if (teamMember != null) {
            teamMember.reviewCode();
        }
    }
}

複製程式碼

最後呼叫如下:

public class Main {

    public static void main(String[] args) {
        TeamAMember teamAMember=new TeamAMember("wuyue");
        TeamLeaderProxy teamLeaderProxy=new TeamLeaderProxy(teamAMember);
        teamLeaderProxy.reviewCode();
    }


}
複製程式碼

輸出結果也很簡單:

Android程式設計師必會技能—執行時動態生成類—之動態代理

那這樣做有什麼好處呢?比方說我們發現最近a小組成員的程式碼好像註釋都比較少,那麼我們就可以在代理類裡面修改我們的程式碼

/**
 * Created by admin on 2018/12/8.
 */
public class TeamLeaderProxy implements TeamMember {
    public TeamLeaderProxy(TeamMember teamMember) {
        //只有a組的成員程式碼 我才review程式碼
        if (teamMember instanceof TeamAMember) {
            this.teamMember = teamMember;
        }
    }

    TeamMember teamMember;


    @Override
    public void reviewCode() {
        if (teamMember != null) {
            System.out.println("註釋程式碼太少了 注意多寫註釋");
            teamMember.reviewCode();
        }
    }
}

複製程式碼

其實這就是一個aop的思想,在代理的過程中加入一些操作,可以在代理的操作之前增加操作,也可以在代理的操作之後增加操作

這裡是靜態代理,靜態代理就是說我們這個代理類是我們之前定義好的,由我們寫的java程式碼然後編譯好的。這裡有什麼缺陷呢?

想象一下,我們除了review組員的程式碼,作為一個leader我們還要幹很多其他事情,比如檢視組員內每週的程式碼提交行數,
檢視組員內最近上下班時間,檢視組員內完成了todo list上面的哪些事情,等等。

如果某一天我希望給自己的這些事情都加入一個時間戳,那不是需要到這些方法裡面依次修改?然後修改完以後再次編譯?

如果這個team還有其他leader呢?那要修改的地方就更多了!

太麻煩了!!

所謂動態代理就是讓我們 動態的去生成一個代理類,這樣我們就不需要依次的修改這些方法了!而且可以根據需要,
在不同的場景下 生成不同的代理!是不是簡單方便很多?

假設我們現在有個需求是,每次leader review的時間都要列印出來給CTO看,CTO想看看leader們的工作狀態,

那麼我們針對此需求可以用動態代理來實現:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Created by admin on 2018/12/8.
 */
public class TeamMemberInvocation<T> implements InvocationHandler {

    T target;

    public TeamMemberInvocation(T target) {
        this.target = target;
    }

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

        System.out.println("準備動態代理執行" + method.getName() + "方法");
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設定日期格式
        System.out.println("執行的時間為" + df.format(new Date()));
        Object result = method.invoke(target, args);
        return null;
    }
}

複製程式碼
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        TeamAMember teamAMember = new TeamAMember("wuyue");

        InvocationHandler teamMemberHandler = new TeamMemberInvocation<TeamMember>(teamAMember);

        TeamMember dynamicProxy = (TeamMember) Proxy.newProxyInstance(TeamMember.class.getClassLoader(), new Class<?>[]{TeamMember.class}, teamMemberHandler);

        dynamicProxy.reviewCode();


    }


}

複製程式碼

然後看一下我們的輸出結果:

Android程式設計師必會技能—執行時動態生成類—之動態代理

誒?你看得到了我們想要的結果,但是我們在程式碼裡面並沒有寫一個實際的代理類啊?這個代理類到底藏到哪裡去了?

動態代理是如何執行的?

我們首先想知道代理的類到底藏在哪 長什麼樣是吧?
所以可以增加一行程式碼

        System.out.println("dynamicProxy 實際的類=="+dynamicProxy.getClass().getName());

複製程式碼

然後檢視結果為: dynamicProxy 實際的類==com.sun.proxy.$Proxy0

檢視我們的程式碼 重要部分

Android程式設計師必會技能—執行時動態生成類—之動態代理

繼續跟:

Android程式設計師必會技能—執行時動態生成類—之動態代理

然後看ProxyClassFactory程式碼

Android程式設計師必會技能—執行時動態生成類—之動態代理

真相大白了。但是因為這個類是動態執行時候生成的,所以我們想看他的位元組碼反編譯以後的程式碼
就得拿到這個class檔案,但是顯然我們是拿不到這個檔案的。但是因為找原始碼找到這裡

知道這個動態類是怎麼生成的,所以我們可以寫一段測試程式碼,看看能不能生成一下這個動態類,
然後把這個類的位元組碼class檔案 輸出到我們的硬碟裡,然後反編譯不就能看到這個類的實際內容了?

public static void writeClassToDisk() {
        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{TeamMember.class});
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("D:\wuyue\out\production\$Proxy0.class");
            fos.write(classFile);
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
複製程式碼

然後執行我們的程式碼,果然,我們終於生成了這個動態的類 位元組碼檔案:

Android程式設計師必會技能—執行時動態生成類—之動態代理

然後當然就是反編譯看看他了:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements TeamMember {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void reviewCode() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("TeamMember").getMethod("reviewCode", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

複製程式碼

知道了動態類是怎麼生成的了,那最後一個問題,動態代理類的方法執行為什麼最終執行的是我們InvocationHandler的invoke方法?

Android程式設計師必會技能—執行時動態生成類—之動態代理
Android程式設計師必會技能—執行時動態生成類—之動態代理

先看h是什麼?

Android程式設計師必會技能—執行時動態生成類—之動態代理
Android程式設計師必會技能—執行時動態生成類—之動態代理

真相大白 原來h 就是我們的InvocationHandler例項啊!

然後再看看m3

Android程式設計師必會技能—執行時動態生成類—之動態代理

到這裡也就真相大白了,其實不難,總結一下:

動態生成的類平時就放在記憶體中,然後實際呼叫的時候通過反射來call他的構造方法,然後就可以得到這個代理類的物件了。
注意這個代理類的物件還持有我們invocationhandler的物件噢,最終call代理類物件方法的時候其實都是call的這個invocationhandler中介類的invoke方法

用代理來在android上做點有趣的事吧

需求:想幹一件壞事,不管同事start 哪個activity 最終都會 start到我的activity上。

要完成這件事首先你要大概瞭解activity的啟動流程。這裡因為篇幅的原因 不作詳細介紹 只大概提一下。

實際上activity start的過程主要是根據下面程式碼而來:

ContextImpl的這個方法:

@Override
    public void startActivity(Intent intent, Bundle options) {
        warnIfCallingFromSystemProcess();

        // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
        // generally not allowed, except if the caller specifies the task id the activity should
        // be launched in.
        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                    + " Is this really what you want?");
        }
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }
複製程式碼

可以看出來這個方法也是由mMainThread.getInstrumentation()來執行最終的方法。
所以這裡我們只要想辦法讓getInstrumentation返回我們自己的Instrumentation
不就可以了嗎?這裡的思路注意和靜態代理其實是差不多的

所以我們可以先構造一個假的Instrumentation

package com.longfor.dynamicproxyexample;

import android.app.Activity;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.IBinder;
import android.util.Log;

public class FakeInstrumentation extends Instrumentation {

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        //這裡為了簡單 判斷一下 如果是同事想啟動的activity 那麼就轉到我自己的activity上來
        if ("com.longfor.dynamicproxyexample.TestOneActivity".equals(className)) {
            className = "com.longfor.dynamicproxyexample.TestTwoActivity";
        }
        return super.newActivity(cl, className, intent);
    }

  
}


複製程式碼

剩下的就是想辦法hook到activitythread物件和更換裡面的Instrumentation物件啦。

package com.longfor.dynamicproxyexample;

import android.app.Application;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MyApplication extends Application {
    static Object activityThreadInstance;

    @Override
    public void onCreate() {
        try {
            //這邊反射的程式碼要注意理解,class.forName的返回的並不是一個物件 而是一個類
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            //而currentActivityThread這個方法是一個靜態方法,因為call 一個類的靜態方法並不需要用物件來call
            //直接用類就可以call ,所以這裡我們用class.forName得到的類就可以直接call這個靜態方法
            //從而不需要像平常的反射程式碼一樣要找到一個物件才能反射call這個物件的方法
            Method currentActivityThread = activityThread.getDeclaredMethod("currentActivityThread");
            activityThreadInstance = currentActivityThread.invoke(null);

            //拿到sCurrentActivityThread例項以後就簡單多了 就直接替換這裡面的mInstrumentation 變數即可
            Field field_instrumentation = activityThreadInstance.getClass()
                    .getDeclaredField("mInstrumentation");
            field_instrumentation.setAccessible(true);
            FakeInstrumentation fakeInstrumentation = new FakeInstrumentation();
            field_instrumentation.set(activityThreadInstance, fakeInstrumentation);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }


        super.onCreate();

    }
}

複製程式碼

看下效果:

Android程式設計師必會技能—執行時動態生成類—之動態代理

然後你就會發現不管你啟動哪個activity最終都會跳轉到我的testtwo這個頁面上了。
實際上大部分外掛框架 hook activity就是根據此思路!

那有人就要問了,你這是靜態代理啊,動態代理在android裡面咋用呢?其實動態代理要用起來就一個最基本的要素
就是你想代理的方法和類 必須是派生自interface的。 這就對我們的程式設計有更高的要求。

比方說,現在有個需求是 需要你做一個日誌的sdk,然後此日誌sdk 要提供給 不同的6個團隊使用,然後
用了一段時間以後,各方面反饋說 都要在自己的地方輸出特殊格式的時間戳,那怎麼做呢?有人說可以升級sdk提供新的方法啊,
根據不同的引數,sdk裡面輸出不一樣的時間戳啊。 那我就要問了,如果以後有n個團隊咋辦?不可能在sdk裡寫n個if else邏輯吧

其實這裡就可以用到動態代理了,完全可以把輸出日誌的方法寫到一個interface裡面,然後誰要用時間戳之類的特性,
就自己動態代理一下自己加,這樣既能保證sdk的純淨,又可以完成需求,還非常簡單,何樂而不為呢?

那麼看完這篇文章,大家可以審視下自己的專案中還有哪些地方可以用動態代理來設計呢?有興趣的同學可以留言給我分享。

相關文章