知乎商业化团队Android侧组件解耦模型「Geoffrey」介绍

2,615 阅读8分钟

背景与主要目标

背景介绍

商业侧广告,广告的实现当然是在ad组件完成。 但都需要将其它业务方的界面作为载体去呈现。

例如一个彩蛋广告(实现在ad组件),是悬浮在首页信息流之上(feed组件),因此不可避免的,会侵入feed组件的代码。

因此一般的组件化开发模式下,需要feed组件依赖ad_api,由ad_api提供相关广告的接口,在feed侧调用,完成交互。

如下伪代码:

/**  
* 首页信息流 (位于feed组件)  
*/  
public class MainPageFragment extends BaseFeedFragment {  
//彩蛋接口,位于ad组件  
private IAdEgg iAdEgg;  
  
@Override  
public void onCreate(@Nullable Bundle savedInstanceState) {  
  super.onCreate(savedInstanceState);  
  //通过组件化路由,获得IAdEgg的实现类  
  InstanceProvider.optional(IAdEgg.class).ifPresent(it -> {  
    iAdEgg = it;  
});  
}  
  
@Override  
public void onResume() {  
  super.onResume();  
  //展示彩蛋  
  iAdEgg.show();  
}  
  
@Override  
public void onStop() {  
  super.onStop();  
  //彩蛋消失  
  iAdEgg.dismiss();  
}  
  
@Override  
public void onDestroy() {  
  super.onDestroy();  
  //彩蛋销毁  
  iAdEgg.destroy();  
}  
}

这样做,在业务上,没有啥问题,实际上,初期我们也是这样处理的。 

当然了,这样做的弊端,也很明显。 

比如,我需要对这个广告的调用进行修改,要将原来 onStop()处执行的 iAdEgg.dismiss() 方法,调整到 onPause()里面去,不可避免的,我需要改动feed组件。

再比如,我们需要新增一个广告,比如增一个浮层广告吧,那会怎么写呢? 伪代码如下:

/**  
* 首页信息流  
*/  
public class MainPageFragment extends BaseFeedFragment {  
//彩蛋接口,位于ad组件  
private IAdEgg iAdEgg;  
//浮层广告接口,位于ad组件  
private IAdFloat iAdFloat;  
  
@Override  
public void onCreate(@Nullable Bundle savedInstanceState) {  
  super.onCreate(savedInstanceState);  
  //通过组件化路由,获得IAdEgg的实现类  
  InstanceProvider.optional(IAdEgg.class).ifPresent(it -> {  
    iAdEgg = it;  
});  
//通过组件化路由,获得IAdFloat的实现类  
  InstanceProvider.optional(IAdFloat.class).ifPresent(it -> {  
    iAdFloat = it;  
});  
}  
  
@Override  
public void onResume() {  
  super.onResume();  
  //展示彩蛋  
  iAdEgg.show();  
  //展示浮层广告  
  iAdFloat.show();  
}  
  
@Override  
public void onStop() {  
  super.onStop();  
  //彩蛋消失  
  iAdEgg.dismiss();  
  //浮层广告消失  
  iAdFloat.dismiss();  
}  
  
@Override  
public void onDestroy() {  
  super.onDestroy();  
  //彩蛋销毁  
  iAdEgg.destroy();  
  //浮层广告销毁  
  iAdFloat.destroy();  
}  
}

随着商业化的特形广告越来越多,会将ad的代码不停的在feed组件进行叠加。

到目前为止,一个宿主界面最多会时承载5种特形广告。并且任何的调用时机的修改,亦或者新增特形广告,本质上都是对feed侧进行修改。

且实际上的广告业务不会向上述伪代码一样简单明了,我方的侵入逻辑甚至增加了首页的维护成本。

结论简而言之,ad组件过度的侵入了feed组件。解耦已经迫在眉睫。

目标&实现效果

针对上述的弊端,设计解耦模型 「Geoffrey」,该模型以APT+动态代理为基础,采用接口注解的方案,一定程度 上实现了 ad组件与其它组件的解耦。

依然以彩蛋广告为例,仅需要在feed侧进行一个注册,就能监听到生命周期:

/**  
* 首页信息流 位于feed组件  
*/  
public class MainPageFragment extends BaseFeedFragment {  
  
@Override  
public void onCreate(@Nullable Bundle savedInstanceState) {  
  super.onCreate(savedInstanceState);  
  //通过组件化路由,获得商业行为收集器  
  InstanceProvider.optional(IAdBehaviorDispatcher.class).ifPresent(it -> {  
     it.register(this);  
});  
}  
}

在ad组件内部完成彩蛋广告的编写

/**  
* 首页彩蛋 位于ad组件  
*/   
@AddServiceHost({"com.zhihu.android.app.feed.ui.fragment.MainPageFragment"})  
@ClassImplementation(type = "AdEgg", relativeInterface = RelativeClassPath.AD_RELATIVE_PATH)  
public class AdEggDemo implements IAdEggObserver {  
    
@Override  
public void onHostDestroy(String fromPage) {  
  //...销毁逻辑  
}  
  
@Override  
public void onHostStop(String fromPage) {  
  //...dismiss逻辑  
}  
  
@Override  
public void onHostResume(String fromPage) {  
  //...show逻辑  
}  
}

由于在AdEggDemo的类注解中,使用 @AddServiceHost 添加了需要监听的宿主界面的全类名:MainPageFragment

因此,当MainPageFragment在其onCreate中调用it.register(this)方法,就会动态创建一个AdEggDemo的实例。

当MainPageFragment依次调用了 onResume()、onStop()、onDestroy() 时,相应的,会调用AdEggDemo的实例的 onHostResume(String fromPage)、onHostStop(String fromPage)、onHostDestroy(String fromPage),很明显,参数fromPage就是MainPageFragment的全类名,用于某些业务下的判断。

并且在宿主调用了onDestroy时,也会自动销毁创建的AdEggDemo实例。


那么,现在我们需要在MainPageFragment 新增一个浮层广告呢? 

不需要再改动feed组件。

只需要在ad组件中新增一个AddFloat实现类,其注解同彩蛋,监听MainPageFragment。

@AddServiceHost({"com.zhihu.android.app.feed.ui.fragment.MainPageFragment"})
@ClassImplementation(type = "AdFloat", relativeInterface = RelativeClassPath.AD_RELATIVE_PATH)
public class AdFloat implements IAdFloatObserver {


    @Override
    public void onHostDestroy(String fromPage) {
        //...销毁逻辑
    }

    @Override
    public void onHostStop(String fromPage) {
        //...dismiss逻辑
    }

    @Override
    public void onHostResume(String fromPage) {
        //...show逻辑
    }

}

也就是说,当 MainPageFragment 的实例在执行生命周期方法时,会既回调 AdFloat实例里的生命周期,也回调AdEggDemo实例里的生命周期。 因为这两个类,都在类注解上加入了对MainPageFragment的监听。

从上面的例子可以看出,其实「Geoffrey」所做的事情,就是建立最终的执行类与宿主的联系,将你需要关心的,宿主的行为,如生命周期,传递到执行类中去!**

有同学又要问了,诶诶诶,你这个方案里,好像只能传递生命周期方法,如果要传递其它行为呢?比如我要监听这个界面的有一个刷新操作,我需要监听这个动作,该怎么做?

当然可以,只是feed需要这么写:

/**  
* 首页信息流  
*/  
public class MainPageFragment extends BaseFeedFragment {  
  
//商业侧行为收集器  
private IBehaviorReceiver behaviorReceiver;  
  
@Override  
public void onCreate(@Nullable Bundle savedInstanceState) {  
  super.onCreate(savedInstanceState);  
  //通过组件化路由,获得商业行为收集器  
  InstanceProvider.optional(IAdBehaviorDispatcher.class).ifPresent(it -> {  
    behaviorReceiver = it.register(this);  
});  
}  
  
@Override  
protected void onRefresh(boolean fromUser) {  
  super.onRefresh(fromUser);  
  //调一下onRefresh  
  behaviorReceiver.onRefresh(fromUser);  
}  
  
}

到这里为止,又多了一个类 IBehaviorReceiver ,这又个啥?

这是个接口类,里面定义了,商业侧广告所需要收集的全部行为!其中当然就包含了生命周期,只是你只要调用了注册方法,就默认会监听生命周期,不再需要自己去写。

看一下已经存在的一些方法:

/**  
* 接口定义类  
* 在这里定义了商业侧需要收集的行为  
*/  
@BehaviorCreator  
public interface IBehaviorReceiver extends IHostLifecycle {  
  
 
@AddBehaviorObserver({"AdFloat"})  
void onRefresh(boolean fromUser);  
  
  
@Override  
void onHostCreate();  
  
  
@AddBehaviorObserver({"AdFloat"})  
@Override  
void onHostDestroy();  
  
  
@Override  
void onHostPause();  
  
@AddBehaviorObserver({"AdFloat"})  
@Override  
void onHostResume();  
  
@Override  
void onHostStart();  
  
@Override  
void onHostStop();  
}

某一个类型的广告,比如浮层广告, 需要关注的宿主界面行为有:onResume、onDestroy、onRefresh 三个方法,只需要在这三个方法上的AddBehaviorObserver注解中,加入自己的一个String类型的type即可! 如上。

这个注解有啥意义呢? 意义在于,通过apt,会在编译期生成一个新接口,并且这个接口,只含有这三个方法。生成的接口如下:

@ServiceType(  
value = "AdFloat",  
relativeInterface = "com.zhihu.android.behavior.IBehaviorReceiver"  
)  
public interface IAdFloatObserver extends IObserver {  
void onRefresh(boolean fromUser, String fromPage);  
  
void onHostDestroy(String fromPage);  
  
void onHostResume(String fromPage);  
}

我们最终真正的执行类,实现这个接口即可!可以往上面翻翻看看用法,是不是浮层广告的执行类实现它了?然后在实现类上打好注解,那就能完成监听了。 或者我们这里再看一遍:

@AddServiceHost({"com.zhihu.android.app.feed.ui.fragment.MainPageFragment"})
@ClassImplementation(type = "AdFloat", relativeInterface = RelativeClassPath.AD_RELATIVE_PATH)
public class AdFloat implements IAdFloatObserver {


    @Override
    public void onHostDestroy(String fromPage) {
        //...销毁逻辑
    }

    @Override
    public void onRefresh(boolean fromUser,String fromPage) {
        //...刷新逻辑
    }

    @Override
    public void onHostResume(String fromPage) {
        //...show逻辑
    }
}

总结一下,这套模型,是一个 「行为收集分发器」,其实是将需要观察到的feed的行为,收敛到位于ad组件的「Geoffrey」中,再由「Geoffrey」进行分发到对应的ad实现中去!

优点在于,我只需要收集一次行为,就可以多次分发,满足商业广告中的业务特点。

行为是怎么收集呢? 是通过在feed中使用IBehaviorReceiver这个接口去收集。 

行为怎么分发呢? 执行类中的注解上,添加了相应的宿主界面全类名。 当宿主调用了该执行为需要关注的方法时(生命周期方法 or 指定的方法),就会进行相应的回调。

到此,基本用法全部讲完。

架构描述

未命名绘图.jpg

要点:

1.通过在接口上打注解,生成了新的接口,通过apt实现,具体使用 javapoet 辅助完成该业务,有兴趣的同学可以自行了解该部份实现,这里暂且不表:

git.in.zhihu.com/yanfang/beh…

这部份代码,只与 生成接口  这项业务有关。

2.如何分发。

使用时已经讲到了,ad组件实际上,只会提供一个接口给其它组件使用,即是:IBehaviorReceiver ,其它组件通过持有该接口的引用,去主动调用ad组件需要观察的方法。

但是该接口没有任何的直接实现类! 有没有觉得很眼熟?

是的,聪明的你一定发现了,这不就是retrofit中对动态代理的应用吗? 在接口方法上加入注解,在动态代理中去让真正的执行类执行,真正的执行类就是实现了apt生成的接口的类。

先看一张使用了「Geoffrey」的类图,这是商业侧目前特形广告使用「Geoffrey」模型的类图,其中「Geoffrey」的内部实现在下面类图中为黑盒。 11.jpg

可以比较明显的看到,feed组件现在只依赖ad_api组件,并且只持有通过IAdBehaviorDispatcher中返回的IBehaviorReceiver接口。

当feed组件通过IBehaviorReceiver调用具体方法时,「Geoffrey」会将该动作,传递到关心此动作的执行类中去!