背景与主要目标
背景介绍
商业侧广告,广告的实现当然是在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 指定的方法),就会进行相应的回调。
到此,基本用法全部讲完。
架构描述
要点:
1.通过在接口上打注解,生成了新的接口,通过apt实现,具体使用 javapoet 辅助完成该业务,有兴趣的同学可以自行了解该部份实现,这里暂且不表:
这部份代码,只与 生成接口 这项业务有关。
2.如何分发。
使用时已经讲到了,ad组件实际上,只会提供一个接口给其它组件使用,即是:IBehaviorReceiver ,其它组件通过持有该接口的引用,去主动调用ad组件需要观察的方法。
但是该接口没有任何的直接实现类! 有没有觉得很眼熟?
是的,聪明的你一定发现了,这不就是retrofit中对动态代理的应用吗? 在接口方法上加入注解,在动态代理中去让真正的执行类执行,真正的执行类就是实现了apt生成的接口的类。
先看一张使用了「Geoffrey」的类图,这是商业侧目前特形广告使用「Geoffrey」模型的类图,其中「Geoffrey」的内部实现在下面类图中为黑盒。
可以比较明显的看到,feed组件现在只依赖ad_api组件,并且只持有通过IAdBehaviorDispatcher中返回的IBehaviorReceiver接口。
当feed组件通过IBehaviorReceiver调用具体方法时,「Geoffrey」会将该动作,传递到关心此动作的执行类中去!