[Android]Ottoソースコードの概要
使用例
ここでは主に以下の例に従って展開します.
初期化
まず、
既定のパラメータはenforcer=ThreadEnforcerです.MAIN,identifier = DEFAULT_IDENTIFIER,handlerFinder = HandlerFinder.ANNOTATED.これらのパラメータがどういう意味か見てみましょう.
ThreadEnforcer
ThreadEnforcerはインタフェースで、enforce()メソッドは現在のスレッドが指定されたスレッドタイプであるかどうかを確認します.
パラメータを持たないコンストラクション関数bus()は、デフォルトのThreadEnforcerを使用します.MAINは、enforce()メソッドがプライマリスレッド上で実行される必要があることを示します.
identifier
identifierはbusの名前だけで、debugは使います.
handlerFinder
HandlerFinderは、登録/逆登録時にSubscriberとProducerを検索するために使用され、後でソースレベルの解析が展開されます.パラメータを持たないコンストラクション関数bus()はデフォルトのHandlerFinderを使用する.ANNOTATEDは、注釈を使用して検索することを示します.
上記のほか、busクラスには2つのメンバー変数handlersByTypeとproducersByTypeがあります.
eventのタイプ(classタイプ)によってevent handleとevent producerを検索するためにそれぞれ使用されます.
登録/逆登録イベント
以下に示すように、Aが購読者としてAnswerAvailableEventを購読するには、busに登録して@Subscribe注記タグコールバックメソッドを使用すればよい.コールバックメソッドでは、可視性がpublicであり、1つのパラメータしかなく、サブスクリプションのeventタイプが必要です.
@Subscribe
まず@Subscribe注記を見てみましょう.
RetentionPolicy.RUNTIMEは実行時の注記であることを示し、ElementType.METHODは注釈方法を表す.
bus.register
registerプロセスをもう一度見てみましょう.
総じてregisterは3つのことをしました:新しいProducerをトリガーします;新しいevent-handler関係を登録します.古いProducerをトリガーします.もう2つ注意しなければならないことがあります.
一般的な使用シーンでは,eventの送信/処理は登録/逆登録操作よりもはるかに頻繁であるため,スレッドの安全を保証する場合には,eventとhandlerを保存する容器としてCopyOnWriteArraySetを用いることで,効率を大幅に向上させることができる.CopyOnWriteコンテナは、読むときにロックされず、書くときにコピーして、書き終わってから元のコンテナに置き換えます.コンテナが書き込み中に読み取り操作(または読み取り中に書き込み操作)が発生した場合、読み取り操作の対象はコンテナのスナップショット(snapshot)です.
registerメソッドはロックされていないため、3-1ではhandlersが存在するかどうかを確認したがputIfAbsentを使用してhandlerを保存する必要がある.
EventProducerとEventHandler
busがHandlerFinderを介してobject上のproducerとsubscriberを検索していることに気づき、次にHandlerFinderの実装を見てみましょう.
ここでfindAllProducersメソッドは、あるイベントタイプに対応するEventProducerを返し、findAllSubscribersは、あるイベントタイプに対応するEventHandlerセットを返します.まずEventProducerとEventHandlerを見てみましょう.
EventProducerはproducerメソッドのパッケージクラスで、ソースコードは以下の通りです.
ここでproduceEventメソッドはeventを取得するために使用されます.Ottoがproduce関数にパラメータを要求しない理由がわかる.
EventProducerと同様に、EventHandlerはevent handlerメソッド(イベントコールバック)のパッケージクラスです.ソースコードは次のとおりです.
ここでhandleEventメソッドはobject上でhandleメソッド(イベントコールバック)を呼び出しeventオブジェクトに転送するために使用されます.Ottoがevent handler関数に1つのパラメータしか要求できない理由がわかる.
dispatchProducerResultToHandler
dispatchProducerResultToHandlerメソッドは、Producerによって生成されたeventを対応するhandlerに配布するために使用される.ソースコードは次のとおりです.
論理は比較的簡単で,主にProducerのproduceEvent()メソッドを用いてeventオブジェクトを取得した後,EventHandlerのhandleEvent()メソッドを呼び出す.
bus.unregister
Busクラスのunregisterメソッドは、オブジェクト上のproducerメソッド、subscriberメソッド、ソースコードを含むターゲットオブジェクトとbusの関連関係を解除するために使用されます.
配達事件
簡単なイベント配信操作は次のとおりです.
postメソッドのソースコード実装を見てみましょう.
注意点:
Eventを送信すると、Event親を購読したSubscriberメソッドも呼び出されます.
イベントは呼び出し元のスレッドのキューに配置され、順次配布されます.
以下、点を分けて詳しく述べる.
flattenHierarchy
post操作を行う場合、eventの親またはインタフェースは、flattenHierarchyメソッドで最初に取得されます.
上からflattenHierarchy()はgetClassesFor()によってconcreteClassのすべての親を深度優先ループで導出していることが分かる.
Dispatch Queue
postメソッドで配信されたeventは,まず現在のスレッドが存在するDispatch Queueに置かれ,順次配布される.Busクラスには、次のメンバー属性があります.
eventsToDispatchはThreadLocalオブジェクトであり、initialValue()メソッドによりeventsToDispatchは新しいスレッドで呼び出されるたびに新しいConcurrentLinkedQueueインスタンスを生成します.eventはenqueueEvent(event,wrapper)メソッドによってqueueに格納されます.次にenqueueEvent()の実装を見てみましょう.
offer()メソッドは、現在のスレッドのqueueの末尾にEventWithHandlerオブジェクトを配置します.offerメソッドとaddメソッドの違いは,挿入できない(例えば空間が足りない)場合にfalseを発揮し,熱は異常を放出するものではない.EventWithHandlerクラスはeventとhandlerの関係を簡単に包装し、以下のように実現した.
次に、dispatchQueuedEventsメソッドの実装を見てみましょう.
注目すべきは、subscribeメソッドが放出したすべての例外がここでキャプチャされ、例外がキャプチャされた後、event配布プロセスは、次のスレッドでpostが呼び出されるまで停止することである.
構造図
以上、Ottoの全体的な構造は以下の図で表すことができる.
2015年11月11日発表
ここでは主に以下の例に従って展開します.
//1. bus ,
Bus bus = new Bus(); // maybe singleton
//2. A(subscriber),answerAvailable() ,
class A {
public A() {
bus.register(this);
}
// public, Event
@Subscribe public void answerAvailable(AnswerAvailableEvent event) {
// process event
}
}
//3. bus
bus.post(new AnswerAvailableEvent(42));
//4. A , B(Producer),produceAnswer()
// subscriber , AnswerAvailableEvent
class B {
public B() {
bus.register(this);
}
// public,
@Produce public AnswerAvailableEvent produceAnswer() {
return new AnswerAvailableEvent();
}
}
初期化
まず、
Bus bus = new Bus()
という文を見てみましょう.対応するソースコードは次のようになります.public Bus() {
this(DEFAULT_IDENTIFIER);
}
public Bus(String identifier) {
this(ThreadEnforcer.MAIN, identifier);
}
public Bus(ThreadEnforcer enforcer, String identifier) {
this(enforcer, identifier, HandlerFinder.ANNOTATED);
}
Bus(ThreadEnforcer enforcer, String identifier, HandlerFinder handlerFinder) {
this.enforcer = enforcer;
this.identifier = identifier;
this.handlerFinder = handlerFinder;
}
既定のパラメータはenforcer=ThreadEnforcerです.MAIN,identifier = DEFAULT_IDENTIFIER,handlerFinder = HandlerFinder.ANNOTATED.これらのパラメータがどういう意味か見てみましょう.
ThreadEnforcer
ThreadEnforcerはインタフェースで、enforce()メソッドは現在のスレッドが指定されたスレッドタイプであるかどうかを確認します.
public interface ThreadEnforcer {
ThreadEnforcer ANY = new ThreadEnforcer() {
@Override
public void enforce(Bus bus) {
// Allow any thread.
}
};
ThreadEnforcer MAIN = new ThreadEnforcer() {
@Override
public void enforce(Bus bus) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("Event bus " + bus +
" accessed from non-main thread " + Looper.myLooper());
}
}
};
void enforce(Bus bus);
}
パラメータを持たないコンストラクション関数bus()は、デフォルトのThreadEnforcerを使用します.MAINは、enforce()メソッドがプライマリスレッド上で実行される必要があることを示します.
identifier
identifierはbusの名前だけで、debugは使います.
handlerFinder
HandlerFinderは、登録/逆登録時にSubscriberとProducerを検索するために使用され、後でソースレベルの解析が展開されます.パラメータを持たないコンストラクション関数bus()はデフォルトのHandlerFinderを使用する.ANNOTATEDは、注釈を使用して検索することを示します.
上記のほか、busクラスには2つのメンバー変数handlersByTypeとproducersByTypeがあります.
private final ConcurrentMap<Class<?>, Set<EventHandler>> handlersByType =
new ConcurrentHashMap<Class<?>, Set<EventHandler>>();
private final ConcurrentMap<Class<?>, EventProducer> producersByType =
new ConcurrentHashMap<Class<?>, EventProducer>();
eventのタイプ(classタイプ)によってevent handleとevent producerを検索するためにそれぞれ使用されます.
登録/逆登録イベント
以下に示すように、Aが購読者としてAnswerAvailableEventを購読するには、busに登録して@Subscribe注記タグコールバックメソッドを使用すればよい.コールバックメソッドでは、可視性がpublicであり、1つのパラメータしかなく、サブスクリプションのeventタイプが必要です.
class A {
public A() {
bus.register(this);
}
@Subscribe public void answerAvailable(AnswerAvailableEvent event) {
// process event
}
}
@Subscribe
まず@Subscribe注記を見てみましょう.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscribe {
}
RetentionPolicy.RUNTIMEは実行時の注記であることを示し、ElementType.METHODは注釈方法を表す.
bus.register
registerプロセスをもう一度見てみましょう.
public void register(Object object) {
if (object == null) {
throw new NullPointerException("Object to register must not be null.");
}
//1. ThreadEnforcer
enforcer.enforce(this);
//2. , object Producer
Map<Class<?>, EventProducer> foundProducers = handlerFinder.findAllProducers(object);
for (Class<?> type : foundProducers.keySet()) {
//2-1. object producer event 。
final EventProducer producer = foundProducers.get(type);
EventProducer previousProducer = producersByType.putIfAbsent(type, producer);
//checking if the previous producer existed
if (previousProducer != null) {
throw new IllegalArgumentException("Producer method for type " + type + " found on type " + producer.target.getClass() + ", but already registered by type " + previousProducer.target.getClass() + ".");
}
//2-2. , event handler, 。
Set<EventHandler> handlers = handlersByType.get(type);
if (handlers != null && !handlers.isEmpty()) {
for (EventHandler handler : handlers) {
dispatchProducerResultToHandler(handler, producer);
}
}
}
//3. object @Subscribe
Map<Class<?>, Set<EventHandler>> foundHandlersMap = handlerFinder.findAllSubscribers(object);
for (Class<?> type : foundHandlersMap.keySet()) {
Set<EventHandler> handlers = handlersByType.get(type);
if (handlers == null) {
//3-1. event , CopyOnWriteArraySet handler event (EventHandler)
//concurrent put if absent
Set<EventHandler> handlersCreation = new CopyOnWriteArraySet<EventHandler>();
handlers = handlersByType.putIfAbsent(type, handlersCreation);
if (handlers == null) {
handlers = handlersCreation;
}
}
//3-2. object event-handler 。
final Set<EventHandler> foundHandlers = foundHandlersMap.get(type);
if (!handlers.addAll(foundHandlers)) {
throw new IllegalArgumentException("Object already registered.");
}
}
//4. object event Producer,
for (Map.Entry<Class<?>, Set<EventHandler>> entry : foundHandlersMap.entrySet()) {
Class<?> type = entry.getKey();
EventProducer producer = producersByType.get(type);
if (producer != null && producer.isValid()) {
Set<EventHandler> foundHandlers = entry.getValue();
for (EventHandler foundHandler : foundHandlers) {
if (!producer.isValid()) {
break;
}
if (foundHandler.isValid()) {
dispatchProducerResultToHandler(foundHandler, producer);
}
}
}
}
}
総じてregisterは3つのことをしました:新しいProducerをトリガーします;新しいevent-handler関係を登録します.古いProducerをトリガーします.もう2つ注意しなければならないことがあります.
一般的な使用シーンでは,eventの送信/処理は登録/逆登録操作よりもはるかに頻繁であるため,スレッドの安全を保証する場合には,eventとhandlerを保存する容器としてCopyOnWriteArraySetを用いることで,効率を大幅に向上させることができる.CopyOnWriteコンテナは、読むときにロックされず、書くときにコピーして、書き終わってから元のコンテナに置き換えます.コンテナが書き込み中に読み取り操作(または読み取り中に書き込み操作)が発生した場合、読み取り操作の対象はコンテナのスナップショット(snapshot)です.
registerメソッドはロックされていないため、3-1ではhandlersが存在するかどうかを確認したがputIfAbsentを使用してhandlerを保存する必要がある.
EventProducerとEventHandler
busがHandlerFinderを介してobject上のproducerとsubscriberを検索していることに気づき、次にHandlerFinderの実装を見てみましょう.
interface HandlerFinder {
HandlerFinder ANNOTATED = new HandlerFinder() {
@Override
public Map<Class<?>, EventProducer> findAllProducers(
Object listener) {
return AnnotatedHandlerFinder.findAllProducers(listener);
}
@Override
public Map<Class<?>, Set<EventHandler>> findAllSubscribers(
Object listener) {
return AnnotatedHandlerFinder.findAllSubscribers(listener);
}
};
Map<Class<?>, EventProducer> findAllProducers(Object listener);
Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener);
}
ここでfindAllProducersメソッドは、あるイベントタイプに対応するEventProducerを返し、findAllSubscribersは、あるイベントタイプに対応するEventHandlerセットを返します.まずEventProducerとEventHandlerを見てみましょう.
EventProducerはproducerメソッドのパッケージクラスで、ソースコードは以下の通りです.
class EventProducer {
final Object target;
private final Method method;
private final int hashCode;
private boolean valid = true;
EventProducer(Object target, Method method) {
if (target == null) {
throw new NullPointerException(
"EventProducer target cannot be null.");
}
if (method == null) {
throw new NullPointerException(
"EventProducer method cannot be null.");
}
this.target = target;
this.method = method;
method.setAccessible(true);
// hashcode, hash()
final int prime = 31;
hashCode = ((prime + method.hashCode()) * prime) + target.hashCode();
}
public boolean isValid() {
return valid;
}
// object unregister
public void invalidate() {
valid = false;
}
public Object produceEvent() throws InvocationTargetException {
if (!valid) {
throw new IllegalStateException(toString() +
" has been invalidated and can no longer produce events.");
}
try {
return method.invoke(target);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof Error) {
throw (Error) e.getCause();
}
throw e;
}
}
}
ここでproduceEventメソッドはeventを取得するために使用されます.Ottoがproduce関数にパラメータを要求しない理由がわかる.
EventProducerと同様に、EventHandlerはevent handlerメソッド(イベントコールバック)のパッケージクラスです.ソースコードは次のとおりです.
class EventHandler {
private final Object target;
private final Method method;
private final int hashCode;
private boolean valid = true;
EventHandler(Object target, Method method) {
if (target == null) {
throw new NullPointerException(
"EventHandler target cannot be null.");
}
if (method == null) {
throw new NullPointerException(
"EventHandler method cannot be null.");
}
this.target = target;
this.method = method;
method.setAccessible(true);
// Compute hash code eagerly since we know it will be used frequently and we cannot estimate the runtime of the
// target's hashCode call.
final int prime = 31;
hashCode = ((prime + method.hashCode()) * prime) + target.hashCode();
}
public boolean isValid() {
return valid;
}
public void invalidate() {
valid = false;
}
public void handleEvent(Object event) throws InvocationTargetException {
if (!valid) {
throw new IllegalStateException(toString() +
" has been invalidated and can no longer handle events.");
}
try {
method.invoke(target, event);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof Error) {
throw (Error) e.getCause();
}
throw e;
}
}
}
ここでhandleEventメソッドはobject上でhandleメソッド(イベントコールバック)を呼び出しeventオブジェクトに転送するために使用されます.Ottoがevent handler関数に1つのパラメータしか要求できない理由がわかる.
dispatchProducerResultToHandler
dispatchProducerResultToHandlerメソッドは、Producerによって生成されたeventを対応するhandlerに配布するために使用される.ソースコードは次のとおりです.
private void dispatchProducerResultToHandler(EventHandler handler, EventProducer producer) {
Object event = null;
try {
event = producer.produceEvent();
} catch(InvocationTargetException e) {
throwRuntimeException("Producer " + producer + " threw an exception.", e);
}
if (event == null) {
return;
}
dispatch(event, handler);
}
protected void dispatch(Object event, EventHandler wrapper) {
try {
wrapper.handleEvent(event);
} catch(InvocationTargetException e) {
throwRuntimeException("Could not dispatch event: " + event.getClass() + " to handler " + wrapper, e);
}
}
論理は比較的簡単で,主にProducerのproduceEvent()メソッドを用いてeventオブジェクトを取得した後,EventHandlerのhandleEvent()メソッドを呼び出す.
bus.unregister
Busクラスのunregisterメソッドは、オブジェクト上のproducerメソッド、subscriberメソッド、ソースコードを含むターゲットオブジェクトとbusの関連関係を解除するために使用されます.
public void unregister(Object object) {
if (object == null) {
throw new NullPointerException("Object to unregister must not be null.");
}
//1. ThreadEnforcer
enforcer.enforce(this);
//2. , object Producer, producersByType invalidate
Map<Class<?>, EventProducer> producersInListener = handlerFinder.findAllProducers(object);
for (Map.Entry<Class<?>, EventProducer> entry : producersInListener.entrySet()) {
final Class<?> key = entry.getKey();
EventProducer producer = getProducerForEventType(key);
EventProducer value = entry.getValue();
if (value == null || !value.equals(producer)) {
throw new IllegalArgumentException(
"Missing event producer for an annotated method. Is " + object.getClass() + " registered?");
}
producersByType.remove(key).invalidate();
}
//3. , object @Subscribe handler, event invalidate
Map<Class<?>, Set<EventHandler>> handlersInListener = handlerFinder.findAllSubscribers(object);
for (Map.Entry<Class<?>, Set<EventHandler>> entry : handlersInListener.entrySet()) {
Set<EventHandler> currentHandlers = getHandlersForEventType(entry.getKey());
Collection<EventHandler> eventMethodsInListener = entry.getValue();
if (currentHandlers == null || !currentHandlers.containsAll(eventMethodsInListener)) {
throw new IllegalArgumentException(
"Missing event handler for an annotated method. Is " + object.getClass() + " registered?");
}
for (EventHandler handler : currentHandlers) {
if (eventMethodsInListener.contains(handler)) {
handler.invalidate();
}
}
currentHandlers.removeAll(eventMethodsInListener);
}
}
配達事件
簡単なイベント配信操作は次のとおりです.
bus.post(new AnswerAvailableEvent(42));
postメソッドのソースコード実装を見てみましょう.
public void post(Object event) {
if (event == null) {
throw new NullPointerException("Event to post must not be null.");
}
//1. ThreadEnforcer
enforcer.enforce(this);
//2. event
Set<Class<?>>dispatchTypes = flattenHierarchy(event.getClass());
//3. event handler, DeadEvent
boolean dispatched = false;
for (Class<?>eventType: dispatchTypes) {
Set<EventHandler> wrappers = getHandlersForEventType(eventType);
if (wrappers != null && !wrappers.isEmpty()) {
dispatched = true;
for (EventHandler wrapper: wrappers) {
//3-1 handler
enqueueEvent(event, wrapper);
}
}
}
//4. event handler, DeadEvent
if (!dispatched && !(event instanceof DeadEvent)) {
post(new DeadEvent(this, event));
}
//5.
dispatchQueuedEvents();
}
注意点:
Eventを送信すると、Event親を購読したSubscriberメソッドも呼び出されます.
イベントは呼び出し元のスレッドのキューに配置され、順次配布されます.
以下、点を分けて詳しく述べる.
flattenHierarchy
post操作を行う場合、eventの親またはインタフェースは、flattenHierarchyメソッドで最初に取得されます.
Set<Class<?>>flattenHierarchy(Class<?>concreteClass) {
Set<Class<?>>classes = flattenHierarchyCache.get(concreteClass);
if (classes == null) {
Set<Class<?>>classesCreation = getClassesFor(concreteClass);
classes = flattenHierarchyCache.putIfAbsent(concreteClass, classesCreation);
if (classes == null) {
classes = classesCreation;
}
}
return classes;
}
private Set<Class<?>> getClassesFor(Class<?> concreteClass) {
List<Class<?>> parents = new LinkedList<Class<?>>();
Set<Class<?>> classes = new HashSet<Class<?>>();
parents.add(concreteClass);
//
while (!parents.isEmpty()) {
Class<?> clazz = parents.remove(0);
classes.add(clazz);
Class<?> parent = clazz.getSuperclass();
if (parent != null) {
parents.add(parent);
}
}
return classes;
}
上からflattenHierarchy()はgetClassesFor()によってconcreteClassのすべての親を深度優先ループで導出していることが分かる.
Dispatch Queue
postメソッドで配信されたeventは,まず現在のスレッドが存在するDispatch Queueに置かれ,順次配布される.Busクラスには、次のメンバー属性があります.
private final ThreadLocal<ConcurrentLinkedQueue<EventWithHandler>> eventsToDispatch =
new ThreadLocal<ConcurrentLinkedQueue<EventWithHandler>>() {
@Override protected ConcurrentLinkedQueue<EventWithHandler> initialValue() {
return new ConcurrentLinkedQueue<EventWithHandler>();
}
};
eventsToDispatchはThreadLocalオブジェクトであり、initialValue()メソッドによりeventsToDispatchは新しいスレッドで呼び出されるたびに新しいConcurrentLinkedQueueインスタンスを生成します.eventはenqueueEvent(event,wrapper)メソッドによってqueueに格納されます.次にenqueueEvent()の実装を見てみましょう.
protected void enqueueEvent(Object event, EventHandler handler) {
eventsToDispatch.get().offer(new EventWithHandler(event, handler));
}
offer()メソッドは、現在のスレッドのqueueの末尾にEventWithHandlerオブジェクトを配置します.offerメソッドとaddメソッドの違いは,挿入できない(例えば空間が足りない)場合にfalseを発揮し,熱は異常を放出するものではない.EventWithHandlerクラスはeventとhandlerの関係を簡単に包装し、以下のように実現した.
static class EventWithHandler {
final Object event;
final EventHandler handler;
public EventWithHandler(Object event, EventHandler handler) {
this.event = event;
this.handler = handler;
}
}
次に、dispatchQueuedEventsメソッドの実装を見てみましょう.
protected void dispatchQueuedEvents() {
// don't dispatch if we're already dispatching, that would allow reentrancy and out-of-order events. Instead, leave
// the events to be dispatched after the in-progress dispatch is complete.
//1. , event
if (isDispatching.get()) {
return;
}
isDispatching.set(true);
try {
while (true) {
//2. EventWithHandler, dispatch 。
EventWithHandler eventWithHandler = eventsToDispatch.get().poll();
if (eventWithHandler == null) {
break;
}
if (eventWithHandler.handler.isValid()) {
dispatch(eventWithHandler.event, eventWithHandler.handler);
}
}
} finally {
isDispatching.set(false);
}
}
注目すべきは、subscribeメソッドが放出したすべての例外がここでキャプチャされ、例外がキャプチャされた後、event配布プロセスは、次のスレッドでpostが呼び出されるまで停止することである.
構造図
以上、Ottoの全体的な構造は以下の図で表すことができる.
+-------------------------+
|Bus(ThreadLocal) |
| +--------------+ |
| |EventProducers| |
| | +-------+ | register +-------+
| | |Produce| <----+-------+Produce|
| | +-------+ | | +-------+
| | +-------+ | |
| | |Produce| | |
| | +-------+ | |
| +--------------+ |
| | |
| event |
| | |
post(event)| +-------v--------+ |
+----------------> Dispatch Queue | |
| +-------+--------+ |
| | |
| event |
| | |
| +------v------+ |
| |EventHandlers| |
| | +---------+ | |
| | |Subscribe| | register +---------+
| | +---------+ <-----+-------+Subscribe|
| | +---------+ | | +---------+
| | |Subscribe| | |
| | +---------+ | |
| +-------------+ |
| |
+-------------------------+
2015年11月11日発表