Android开发经验谈架构算法设计模式和编程理论Android 面试专辑

代理模式——基础的设计

2019-07-07  本文已影响289人  彭旭锐

  代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧。在日常应用中,代理模式的使用频率非常高,许多其他的设计模式本质上是在特定的上下文中,对代理模式进行的针对性优化,由此可见其重要性。
  本文的第一节阐述了代理模式的定义,如果你已经对代理模式(静态代理)比较熟悉了,可以跳过这个部分;第二节讨论了动态代理及其实现原理;第三个节提出了一些扩展思路的要点,其中包含了多数人(和一部分书籍)容易犯的思维桎梏,请不要错过它。第四节列举了其他一些常见的设计模式,你能理解它们和代理模式的微妙关系吗?最后的部分是参考的文献资料。

1. 代理的定义

  代理模式用于解决两种问题:

  这是两种非常朴素的场景,正因如此,才会常常从其他设计模式中发现代理模式的影子,参考下面的UML类图和时序图:

代理模式UML 类图 代理模式UML 时序图

  从类图看,代理对象(Proxy)通过组合的方式持有基础对象(RealSubject)的引用,并且实现了Subject接口,从客户端的角度,Proxy代理对象可以作为Subject接口的替代品。
  从时序图看,客户端依靠代理对象(Proxy)工作,代理对象将请求转发给基础对象(RealSubject),在这个例子中,代理对象在转发之前先进行了访问控制。到这里,我们可以把代理模式解释为:由代理对象组合基础对象,控制对基础对象的访问,扩展新的功能。
  从OOP的原则看,代理模式主要体现了:

继承和组合的区别

继承和组合是实现代码复用和扩展的两种方式,两者的差别体现在在灵活性和封装性上。
灵活性上,继承在编译期静态地定义了类的层次结构,直白明了,易于使用,但是反过来看,继承不能在运行期改变复用的代码;相比之下,组合在运行期可以通过替换引用的对象的方式来修改复用的对象。
封装性上,继承(白盒复用)有侵入性,子类必须全盘接收父类的(非私有)内部实现,一定程度上破坏了封装;使用组合(黑盒复用)时对象间通过接口相互作用,对象的封装性得到保护。

2. 动态代理

2.1 动态代理的定义

  Java中有两种代理模式:静态代理和动态代理,上一节提到的实现方式属于静态代理。
  静态代理由程序员或代码生成工具生成代理类,编译之后生成Class文件,代理关系在编译期就已经绑定,一个代理关系是一个代理类对应一个基础接口。
  假设项目中需要给很多类扩展相同的功能,比如所有和网络有关的业务类需要打印请求日志。使用静态代理时,为了给每个业务类添加代理,做法是为每个业务类抽象一个接口,对应地新建一个代理类,并在代理类中实现日志功能。例如:

public interface HttpApi {
    String get(String url);
}
​
public class RealModule implements HttpApi {
     @Override
     public String get(String url) {
         return "result";
     }
}
​
public class Proxy implements HttpApi {
    private HttpApi target;

    Proxy(HttpApi target) {
        this.target = target;
    }

    @Override
    public String get(String url) {
        // 扩展的功能
        Log.i("http-statistic", url);
        // 访问基础对象
        return target.get(url);
    }
}

  假设有一个OtherHttpApi接口,可以选择新建一个OtherProxy代理类,退而求其次也可以选择将代码写在Proxy类中,让Proxy类继续代理OtherHttpApi。无论哪种选择,都无法规避静态代理的两个缺点:一是重复性,在程序规模稍大时,需要代理的方法越多,重复的模板代码就越多;二是脆弱性,基础接口一旦改动,除了所有业务类需要改动外,代理类也必须改动。

Kotlin提供的by操作符可以在实现代理时减少冗余的模板代码,但是很遗憾,在需要装饰方法时收效甚微。

  有没有办法规避这些缺点呢?
  我们知道,JVM在真正开始引用一个类之前,需要先经过加载、连接、初始化,将描述类信息的Class文件转换为内存中Class对象,这就是JVM的类加载机制。(一般地)Class文件在编译后生成,类加载的工作是在运行时完成,虽然会稍微增加一些性能开销,但是却给Java带来的动态扩展的特性,提供了高度的灵活性。Class文件(而不是源代码)是JVM的语言,Java、Groovy、Kotlin等其他语言都可以把代码编译称Class文件。既然如此,对于JVM来说,代理类的源代码并不是必须的,只要有办法得到代理类的Class文件,就可以执行类加载。

Class文件是一个通俗的说法,表示存储程序的二进制字节码(ByteCode),但不一定以磁盘文件的形式存在,可以来自网络上的二进制流,甚至在运行时计算生成。不论哪种来源,Class文件总在类加载的加载阶段转变为内存中byte[]的形式,经过连接和(可选的)初始化后,一个类才算加载完成。

  动态代理(Dymanic Proxy API)是JDK1.3中引入的特性,核心API是java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。它利用反射机制在运行时生成代理类的字节码,为Java平台在带来了运行时动态扩展对象行为的能力。回到前面例子:

public class ProxyFactory {
    public static HttpApi getProxy(HttpApi target) {
        return (HttpApi) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
        private HttpApi target;

        LogHandler(HttpApi target) {
            this.target = target;
        }
        // method底层的方法无参数时,args为空或者长度为0
        @Override
        public Object invoke(Object proxy, Method method, @Nullable Object[] args)       
               throws Throwable {
            // 扩展的功能
            Log.i("http-statistic", (String) args[0]);
            // 访问基础对象
            return method.invoke(target, args);
        }
    }
}

  如果需要兼容多个业务接口,可以在生成代理的接口处使用泛型:

public class ProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T getProxy(T target) {
        return (T) Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(),
        new LogHandler(target));
    }

    private static class LogHandler<T> implements InvocationHandler {
        ...
    }
}
// 客户端调用:
HttpAPi proxy = ProxyFactory.getProxy<HttpApi>(target);
OtherHttpApi proxy = ProxyFactory.getProxy<OtherHttpApi>(otherTarget);

  通过泛型参数传递不同的类型,客户端可以按需实例化不同类型的代理对象。基础接口的所有方法都统一到InvocationHandler#invoke() 处理,即使有多个基础业务需要代理,也不需要编写过多重复的模板代码;当基础接口变更时,同步改动代理并不是必须的,从而规避了重复性和脆弱性。
  回顾下静态代理和动态代理:

2.2 动态代理的源码分析

  这一节,我们将分析java.lang.reflect.Proxy源码,理解代理类Class文件是如何生成的,以及它如何将方法调用统一到java.lang.reflect.InvocationHandler接口中处理。
  先列出Proxy类的public方法:

Proxy类的public方法

为什么需要指定ClassLoader对象?

ClassLoader相当于类的命名空间,类的唯一性由它本身和加载它的ClassLoader确定。一个Class文件如果由两个ClassLoader加载,结果是两个独立的类。

Proxy.java:
public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces){
    final Class<?>[] intfs = interfaces.clone();
    ...
    // 重点:获得代理类Class对象
    return getProxyClass0(loader, intfs);
}
​
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h){
    ...
    final Class<?>[] intfs = interfaces.clone();
    // 重点:获得代理类Class对象
    Class<?> cl = getProxyClass0(loader, intfs);
    ...
    // 获得代理类构造器
    // private static final Class<?>[] constructorParams = { InvocationHandler.class };
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    ...
    // 创建实例
    return newInstance(cons, ih);
}
​
public static boolean isProxyClass(Class<?> cl) {
    // 检查时Proxy的子类,并且proxyClassCache中有缓存
    return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
​
public static InvocationHandler getInvocationHandler(Object proxy){
     // 检查是代理对象
    if (!isProxyClass(proxy.getClass())) {
        throw new IllegalArgumentException("not a proxy instance");
    }
    final Proxy p = (Proxy) proxy;
    final InvocationHandler ih = p.h;
    ...
    // 返回InvocationHandler对象
    return ih;
}

  可以看到,Proxy#getProxyClass() 和 Proxy#newProxyInstance() 都调用了 Proxy#getProxyClass0() 获得代理类Class对象,后者还获取了一个以InvocationHandler为参数的构造器,最终创建并返回了一个代理对象实例。再看Proxy#getProxyClass0() 源码:

Proxy.java:
private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
    ...
    // 从缓存中获取代理类,如果缓存未命中,则通过ProxyClassFactory生成代理类
    return proxyClassCache.get(loader, interfaces);
}
private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>{
    // 代理类命名前缀
    private static final String proxyClassNamePrefix = "$Proxy";
    // 代理类命名后缀,从0递增(原子Long)
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            // 验证参数interfaces和ClassLoder中加载的是同一个类
            // 验证参数interfaces是接口类型
            // 验证参数interfaces中没有重复项
            // 否则抛出IllegalArgumentException
        }
        // 验证所有non-public接口来自同一个包

        //(一般地)代理类包名
        // public static final String PROXY_PACKAGE = "com.sun.proxy";
        String proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";

        // 代理类的全限定名
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 重点:生成字节码数据
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
        // 重点:从字节码生成Class对象
        return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length); 
    }
}

  ProxyClassFactory#defineClass0()是native方法(类似地,ClassLoader#defineClass()最终委派的方法也是native方法),从字节码到Class对象的这部分类加载过程由JVM继续进行。重点看ProxyGenerator#generateProxyClass()如何生成代理类字节码数据:

Tip:Android系统中生成字节码和从字节码生成Class对象的步骤都在native实现:
private static native Class<?> generateProxy(…)
对应的native方法:dalvik/vm/native/java_lang_reflect_Proxy.cpp

ProxyGenerator.java:
public static byte[] generateProxyClass(final String var0, Class[] var1) {
    ProxyGenerator var2 = new ProxyGenerator(var0, var1);
    // 是否保存至磁盘文件 
    final byte[] var3 = var2.generateClassFile();
    if (saveGeneratedFiles) {
        ...
         return var3;
    }
}
private byte[] generateClassFile() {
    // 只代理Object的hashCode、equals和toString
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);

    // 代理接口的每个方法
    ...
    for(var1 = 0; var1 < this.interfaces.length; ++var1) {
        ...
    }
    // 重点:添加带有InvocationHandler参数的构造器
    this.methods.add(this.generateConstructor());
    var7 = this.proxyMethods.values().iterator();
    while(var7.hasNext()) {
        ...
        // 重点:在每个代理的方法中调用InvocationHandler#invoke()
    }
    ByteArrayOutputStream var9 = new ByteArrayOutputStream();
    DataOutputStream var10 = new DataOutputStream(var9);
    ...
    return var9.toByteArray();
}

  可以看到ProxyGenerator#generateProxyClass() 是一个静态public方法,我们直接调用,并将代理类Class文件写入磁盘文件,使用IntelliJ IDEA的反编译功能查看源代码:

// Client:
byte[] classFile = ProxyGenerator.generateProxyClass("$proxy0",new Class[]{HttpApi.class});
// 直接写入项目路径下,方便使用IntelliJ IDEA的反编译功能
String path = "/Users/pengxurui/IdeaProjects/untitled/src/proxy/HttpApi.class";
try(FileOutputStream fos = new FileOutputStream(path)){
    fos.write(classFile);
    fos.flush();
    System.out.println("success");
} catch (Exception e){
    e.printStackTrace();
    System.out.println("fail");
}
public final class $proxy0 extends Proxy implements HttpApi {
    //反射的元数据Method存储起来,避免重复创建
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

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

    /**
     * Object#hashCode()
     * Object#equals(Object)
     * Object#toString()
     */

    // 实现了HttpApi接口
    public final String get() throws  {
        try {
            //转发到Invocation#invoke()
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            //Object#hashCode()
            //Object#equals(Object)
            //Object#toString()
            m3 = Class.forName("HttpApi").getMethod("get");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

  到这里就走完生成代理类Class文件的流程,可以得出:
  动态生成的代理类命名为com.sun.proxy$Proxy[从0开始的数字](例如:com.sun.proxy​$Proxy0),继承了java.lang.reflect.Proxy类,有一个参数为InvocationHandler接口的构造器,同时实现了指定的基础接口,并将方法调用转发给InvocationHandler#invoke()。参考UML类图,仔细体会图中红色的箭头,代理类(Prox$0)和HttpApi接口的代理关系是在运行时才确定的:

动态代理UML 类图

3. 扩展思路的要点

3.1 面向切面编程

  OOP(Object Oriented Progarmming,面向对象编程)是一种使用封装、继承、多态将业务处理过程抽象为一个个清晰的层级结构的编程思想。
  AOP(Aspect Oriented Programming,面向切面编程)是一种通过分离横切关注点(cross-cutting concerns)来维持程序模块化的编程思想。
  AOP是OOP的补充和完善,AOP更关注业务处理中的横切步骤或阶段,将影响多个步骤的横切步骤从核心业务模块单元单独提炼出来,命名为切面(Aspect),降低了横切步骤的侵入性。比如将日志、安全控制、异常处理、拦截点作为横切关注点单独提出,既有利于单独维护,又不会改动原有的业务模块。静态代理和动态代理都是实现AOP编程的方法。

3.2 视图的概念

  回顾静态代理和动态代理的UML类图,我们有RealModule类实现了HttpApi接口。但是,必须有这层实现关系吗,如果没有,还算是代理模式吗?

动态代理UML 类图

  如果有人指着你的程序说:“不是代理模式!这里少了一个接口”,你会乖乖点头吗?提醒一下,设计模式本身并不是目的,解决问题才是。不要为了设计而设计!

人生有时候,乖乖的只是呆子,不乖的却是才子啊! —— 《蔡康永的说话之道2》

  很多人(和一部分书籍)存在两个误解:

package com.domain;
public interface HttpApi {
    String get();
}

// 另一个包的non-public接口
package com.domain.inner;
/**non-public**/interface OtherHttpApi{
    String get();
}

package com.domain.inner;
// OtherHttpApiImpl类没有实现HttpApi接口或者没有实现任何接口
public class OtherHttpApiImpl  /**extends OtherHttpApi**/{
    public String get() {
        return "result";
    }
}

// Client:
 HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();
  
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO:扩展的新功能
        // IllegalArgumentException: object is not an instance of declaring class
        return method.invoke(impl,args);
    }
});
api.get();

  这个例子里,OtherHttpApiImpl类因为历史原因没有实现HttpApi接口,虽然方法签名与HttpApi接口的方法签名完全相同,但是遗憾,参数method来自于HttpApi接口,而不是OtherHttpApiImpl类。也有补救的办法,找到HttpApi接口中签名相同的Mthod,使用这个Method来转发调用:

HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO:扩展的新功能
        if (method.getDeclaringClass() != impl.getClass()) {
            // 找到相同签名的方法
            Method realMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
            return realMethod.invoke(impl, args);
        }else{
            return method.invoke(impl,args);
        }
    }
});

  其实没有人规定一定要调用Method#invoke(...)转发调用到业务对象,直接调用和反射调用都可以访问基础对象!再次回顾代理模式的定义:由代理对象组合基础对象,控制对基础对象的访问,扩展新的功能。
  这里提到“视图”的概念,即允许将任何对象视为任何接口,当基础对象和接口不匹配时,内部使用一些映射。也许你会理解为适配器模式和外观模式的思想,exactly!只是后两者通常是一种静态转换,动态代理的威力在于,如前所述:一个代理同时处理N多个基础接口,代理关系在运行时确定。试想一下,一个(或少数)基础对象可以代理整个应用的接口,或者数以万计的应用的接口。What a magic! 有名Retrofit框架就是这样的例子。

4. 相关的设计模式

  文章开头提到,许多其他设计模式本质上是在特定的上下文中,对代理模式进行的针对性优化。设计模式之间确实有相似的地方,关键的区别在于它们关注/强调的目的不同。再提醒一次,设计模式是指导解决软件问题的思想,而不是约束。

4.1 装饰模式

  装饰模式(Decorate Pattern)指动态地给基础对象添加额外的功能,相对于代理模式,更多强调的是增强所装饰对象的功能。

4.2 外观模式

  外观模式(Facade Pattern),封装子系统间的逻辑和交互,将行为开放给高层接口,强调的是最小接口原则,降低了高层模块对于子系统的耦合,对外观接口的封装也使得接口更易用。外观模式在开发过程中运用频率非常高,比如很多框架为了降低用户的使用成本,会提供一个统一的高层接口。
  Android中的Context类就是外观模式的典型例子,Context是一个抽象类,通常翻译为上下文,指程序运行环境的基本信息。Context类有两个直接子类,ContextImpl类和ContextWrapper类,参考如下UML类图:

Context类UML 类图

  子类ContextImpl类就是外观类,真正的方法实现并不在ContextImpl类中,而在其内部封装的多个子系统中,比如AMS管理Activity(及其他组件)状态,PMS管理应用包相关的信息,Resource管理资源系统。另一个子类ContextWrapper有三个直接子类:Service、ContextThemeWrapper、Application,分别提供了不同的扩展,参考以下源码:

ActivityThread.java:
private void handleBindApplication(AppBindData data){
    ...
    // 创建Application实例
    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;
    ...
    // 重点:回调onCreate()
    mInstrumentation.callApplicationOnCreate(app);
    ...
}

public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
    // 避免重复构造
    if (mApplication != null) {
        return mApplication;
    }
    ...
    // 创建基础对象ContextImpl
    ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
    // 重点:创建Application实例
    app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
    appContext.setOuterContext(app);
    ...
    return app;
}

Instrumentation.java:
public Application newApplication(ClassLoader cl, String className, Context context){
    Application app = getFactory(context.getPackageName())
                .instantiateApplication(cl, className);
    // 重点:设置基础对象
    app.attach(context);
    return app;
}
ActivityThread.java:
/**Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 获取ActivityInfo信息
    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }
    // 获取Component信息
    ComponentName component = r.intent.getComponent();
    if (component == null) {
        component = r.intent.resolveActivity(mInitialApplication.getPackageManager());
        r.intent.setComponent(component);
    }
    // 重点:创建基础对象ContextImpl
    ContextImpl appContext = createBaseContextForActivity(r);
    // 创建Activity实例
    java.lang.ClassLoader cl = appContext.getClassLoader();
    Activity activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
    ...
    // 获取Application对象
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);  
    appContext.setOuterContext(activity);
    // 重点:设置基础对象
    activity.attach(appContext, this, app, ...);
    // 设置Intent
    if (customIntent != null) {
    activity.mIntent = customIntent;
    }
    ...
    // 设置主题
    int theme = r.activityInfo.getThemeResource();
    if (theme != 0) {
        activity.setTheme(theme);
    }
    // 重点:回调onCreate()
    if (r.isPersistable()) {
        mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
    } else {
        mInstrumentation.callActivityOnCreate(activity, r.state);
    }
    ...
}   
ActivityThread.java:
private void handleCreateService(CreateServiceData data) {
    ...
    // 重点:创建Service实例
    Service service = packageInfo.getAppFactory()
                    .instantiateService(cl, data.info.name, data.intent);
    ...
    // 创建基础对象ContextImpl
    ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
    context.setOuterContext(service);
    // 获取Application实例
    Application app = packageInfo.makeApplication(false, mInstrumentation);
    // 重点:设置基础对象
    service.attach(context, this, data.info.name, data.token, app,
                    ActivityManager.getService());
    // 重点:回调onCreate()
    service.onCreate();
    ...
}

  分析Application、Service、Activity的初始化源码,可以看到它们处理基础对象ContextImpl的流程是非常相似的:创建基础对象ContextImpl,并调用attach(),内部调用ContextWrapper#attachBaseContext() ,最终建立起与ContextImpl的代理关系。

Activity.java:
public class Activity extends ContextWrapper{
    final void attach(Context context,ActivityThread aThread,Application application,...){
         ...
        attachBaseContext(context);
        mMainThread = aThread;
        mApplication = application;c
    }
}

ContextWrapper.java:
public class ContextWrapper extends Context{
    private Context mBase;
        protected void attachBaseContext(Context base){
            ...
            mBase = base;
        }
}

5. 参考

上一篇下一篇

猜你喜欢

热点阅读