ネッティーは(次)プロセスを開始する-Netty最も詳細なソースコード解析の歴史ではありません

免責事項:この記事は、ブロガーオリジナル記事です転載を歓迎します。https://blog.csdn.net/guo_xl/article/details/86671309

NIOベースクラス

始める前に、あなたは、Java NIOの次のクラスを理解する必要があります

  • セレクタ
  • SelectableChannel
  • SelectionKey

あなたは見えるかもしれJAVA NIOの基本的なクラスを

登録プロセスサーバーを起動

そして、サーバの前面には、登録プロセスを開始するには、

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
 public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            ..
            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                  //注册的时候会往taskQueue里放入一个注册的线程
                    eventLoop.execute(new Runnable() {
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                   ...
                }
            }
        }

    private void register0(ChannelPromise promise) {
            doRegister();
    }
 }       

登録はしているServerSocketChannelように登録selectorレーン

//为什么这里是0而是SelectionKey.OP_ACCEPT?
//在AbstractNioChannel.doBeginRead里会改,这个bind里再分析
 protected void doRegister() throws Exception {
   selectionKey = javaChannel().register(eventLoop().selector, 0, this);
   }

実際には、以前の分析が追加されて登録されていた内部。NioEventLooptaskQueue

見てNioEventLoop上記のこのタスクの登録を実行する方法である()内で実行します。

二つの方法のNioEventLoopのrunメソッドは、実際には、実装

  • processSelectedKeys()を実行するI / Oタスクを等、受け入れ接続し、読み取り、書き込みなどすなわち準備とSelectionKeyイベント、

  • runAllTask​​s()を実行する非IOタスクはタスクタスクキュー、例えばレジスタ0、bind0タスクに加え

次のコード注釈を考えてみましょう

 protected void run() {
        for (;;) {
            try {
                switch
                //计算出每次循环要干什么事
                (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    //处理I/O操作
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                //一次处理包括了processSelectedKeys()+runAllTasks() ioRatio是指processSelectedKeys()即处理IO事件的占据的比例, 默认是50,
                final int ioRatio = this.ioRatio;
                //如果是100,那就是处理完所有的I/O 再处理完所有的非I/O
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                //如果不是100,那就是I/O处理了时间为t1,非I/O处理的时间为t1*(100 - ioRatio) / ioRatio,
                //当然默认情况下I/O处理了时间为t1,则非I/O处理的时间也只能为t1
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

フレーズの分析
selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())

selectStrategy是DefaultSelectStrategy

final class DefaultSelectStrategy implements SelectStrategy {
    static final SelectStrategy INSTANCE = new DefaultSelectStrategy();

    private DefaultSelectStrategy() { }

    
    //hasTasks为false,那表示taskQueue里没任务,即没非IO任务,那就执行IO任务.
    //那为什么hasTasks为true要selectSupplier.get()?这个在后面会解答
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }

次のようにDefaultSelectStrategyが定義されています

 private final IntSupplier selectNowSupplier = new IntSupplier() {
        @Override
        public int get() throws Exception {
            return selectNow();
        }
    };
   
int selectNow() throws IOException {
        try {
           //这方法不会阻塞会立即返回
           //返回的值是指有多少个channel ready for handle
            return selector.selectNow();
        } finally {
            // restore wakup state if needed
            if (wakenUp.get()) {
                selector.wakeup();
            }
        }
    }    

ロジックは何のミッションが存在しないタスクキューで一緒に見ている、私は何の操作コレクションはありません任意のセレクタで確認しに行ってきました。)最初addTask(以降)(hasTasksように、登録サーバーチャネルに追加チャネルセレクタが登録されていない。真であるので、selector.selectNow()リターンが0です。この場合、それはに直接移動します

runAllTasks(ioTime * (100 - ioRatio) / ioRatio);

非I / Oを処理するために属するレジスタは、つまり、私たちはしますrunAllTasksに対処します。
次runAllTask​​s()のコメントを考えてみましょう

//取消息然后再`safeExecute(task)`直接运行。运行完channel就注册到了selector上了。
 protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();
        Runnable task = pollTask();
        if (task == null) {
            afterRunningAllTasks();
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            //跟进去就是task.run(),注意不是start()
            safeExecute(task);
            ...
        }

        afterRunningAllTasks();
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

runAllTask​​sは、実行ServerSocketChannelそれはに登録されているselectorこと。

上述runAllTask​​s()は、非I / Oタスクの処理であるとして、それは、次であるprocessSelectedKeys()I / Oロジック次コード処理

private void processSelectedKeys() {
      //debug 发现selectedKeys非空,那这个是怎么来的?
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }

SelectedKeys最初の質問は、割り当てが行くところこれは、空ではないでしょうか?

JDKを得る際にSelectorselectedKeysに置き換えたときの参照で定義されてSelectorImpl selectedKeyspublicSelectedKeys、見れば
JAVA NIOの基本的なクラスをセレクタで、あなたはセレクタがそのうちの1つのセットの3種類を、維持知っているselectedKeys選択の実装では、 ()またはselectNow()または登録証明書を操作することになるとき(長い)を選択し(SelectionKey)コレクションにselectedKeysは、このフィールド。

次のようにあるいはコードであります

private Selector openSelector() {
        final Selector selector;
        try {
            selector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }

        if (DISABLE_KEYSET_OPTIMIZATION) {
            return selector;
        }

        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    return Class.forName(
                            "sun.nio.ch.SelectorImpl",
                            false,
                            PlatformDependent.getSystemClassLoader());
                } catch (ClassNotFoundException e) {
                    return e;
                } catch (SecurityException e) {
                    return e;
                }
            }
        });

        if (!(maybeSelectorImplClass instanceof Class) ||
                // ensure the current selector implementation is what we can instrument.
                !((Class<?>) maybeSelectorImplClass).isAssignableFrom(selector.getClass())) {
            if (maybeSelectorImplClass instanceof Exception) {
               ...
            }
            return selector;
        }

        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;

        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    //要替换的字段为selectedKeys
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    //要替换的字段为publicSelectedKeys
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                    selectedKeysField.setAccessible(true);
                    publicSelectedKeysField.setAccessible(true);

                   //执行替换 selectedKeysField.set(selector, selectedKeySet);
                   //执行替换 publicSelectedKeysField.set(selector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                   ...
                }
            }
        });

        if (maybeException instanceof Exception) {
            ...
        } else {
            selectedKeys = selectedKeySet;
            logger.trace("instrumented a special java.util.Set into: {}", selector);
        }

        return selector;
    }
public abstract class SelectorImpl extends AbstractSelector {
    protected Set<SelectionKey> selectedKeys = new HashSet();
    private Set<SelectionKey> publicSelectedKeys;
}

これも説明した理由上記質問hasTasks = trueの場合、なぜ直接固定値に、例えばhasTasks ? 0 : SelectStrategy.SELECT;、むしろselectSupplier.getに(下)。
selectSupplier.getは()にこの点をNioEventLoop.selectedKeysますのでSelectorImpl.selectedKeys

元selectedKeysはHashSetので、交換後に次のクラス、またAbstractSet継承し、それが直接交換することができます。ただ、リファレンスのように、なぜフリップ()一時的に理解されていないがあるのはなぜなぜ?二つの配列keysA、keysAを行う?置き換えます

このクラスは非常に簡単です、addメソッドを書き換えます

final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
  private SelectionKey[] keysA;
    private int keysASize;
    private SelectionKey[] keysB;
    private int keysBSize;
    private boolean isA = true;

    SelectedSelectionKeySet() {
        keysA = new SelectionKey[1024];
        keysB = keysA.clone();
    }

    @Override
    public boolean add(SelectionKey o) {
        if (o == null) {
            return false;
        }

        if (isA) {
            int size = keysASize;
            keysA[size ++] = o;
            keysASize = size;
            if (size == keysA.length) {
                doubleCapacityA();
            }
        } else {
            int size = keysBSize;
            keysB[size ++] = o;
            keysBSize = size;
            if (size == keysB.length) {
                doubleCapacityB();
            }
        }

        return true;
    }

    private void doubleCapacityA() {
        SelectionKey[] newKeysA = new SelectionKey[keysA.length << 1];
        System.arraycopy(keysA, 0, newKeysA, 0, keysASize);
        keysA = newKeysA;
    }

    private void doubleCapacityB() {
        SelectionKey[] newKeysB = new SelectionKey[keysB.length << 1];
        System.arraycopy(keysB, 0, newKeysB, 0, keysBSize);
        keysB = newKeysB;
    }

    SelectionKey[] flip() {
        if (isA) {
            isA = false;
            keysA[keysASize] = null;
            keysBSize = 0;
            return keysA;
        } else {
            isA = true;
            keysB[keysBSize] = null;
            keysASize = 0;
            return keysB;
        }
    }

    @Override
    public int size() {
        if (isA) {
            return keysASize;
        } else {
            return keysBSize;
        }
    }
}
 public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
       
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }

私はなぜselectedKeysが空を知って、どこに投稿します。このコードは、processSelectedKeysOptimized(selectedKeys.flip())に行くだろう。

private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }

分析processSelectedKeysOptimized

private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
//死循环,当没任务的时候跳出
        for (int i = 0;; i ++) {
            final SelectionKey k = selectedKeys[i];
            if (k == null) {
                break;
            }
            final Object a = k.attachment();

            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            ...
        }
    }
 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop();
            } catch (Throwable ignored) {
                ...
                return;
            }
            if (eventLoop != this || eventLoop == null) {
                return;
            }
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
            // the NIO JDK channel implementation may throw a NotYetConnectedException.
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
                // See https://github.com/netty/netty/issues/924
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                ch.unsafe().forceFlush();
            }

            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
                if (!ch.isOpen()) {
                    // Connection already closed - no need to handle write.
                    return;
                }
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

スレッド・スタック解析

スレッドスタックからソースコードの上記の分析は、より明確になります

telnet 127.0.0.1 8888の後に、コードを呼び出すスレッドスタック以下。

"boss-1-1@1468" prio=5 tid=0xd nid=NA runnable
  java.lang.Thread.State: RUNNABLE
	  at io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor.channelRead(ServerBootstrap.java:254)
	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:373)
	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359)
	  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:351)
	  at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:373)
	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359)
	  at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
	  at io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe.read(AbstractNioMessageChannel.java:93)
	  at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:651)
	  at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:574)
	  at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:488)
	  at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:450)
	  at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:873)
	  at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:144)
	  at java.lang.Thread.run(Thread.java:748)

下に行わ構文解析
1。

  at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:450)

実行方法は、死のサイクルである
2。

  at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:651)
  at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:574)
  at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:488)

(後)runTaskは()NioEventLoop.selectedKeysにselector.selectNowに記入します
3。

    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
        	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:373)
        	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359)
        	  at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
        	  at io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe.read(AbstractNioMessageChannel.java:93)

これは、パイプライン、HeadContext.channelReadの実装からHeadContextを見つけるために、パイプラインでNioServerChannelを見つけることです、それは以下の呼び出し

    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }
    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }

実際には、二重に次のコンテキストでリストをリンクされ、

    at io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor.channelRead(ServerBootstrap.java:254)
    	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:373)
    	  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:359)
    	  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:351)

次のコンテキストがServerBootstrapAcceptorを見つけることです、これはServerBootstrapAcceptorは内部パイプラインに追加どこにあるのでしょうか?

    //初始化channel里
    void init(Channel channel) throws Exception {
       ....
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }
              
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

この例では、我々は3つのしかコンテキストを持って、次のように
ここに画像を挿入説明
ここに画像を挿入説明
どこ設定値を理解しています。
当然のことながら、私たちはServerBootstrapAcceptor.channelReadを何方法を分析するために行くだろう

1.ブレークポイントServerBootstrapAcceptor 237行の
ここに画像を挿入説明
子供がio.netty.channel.socket.nio.NioSocketChannelのある
child.pipeline()addLast(childHandler);.
ChildHandler = $ rechard.learn.netty.demo.welcome.WelcomeServer。1はWelcomeServerで定義されています
ここに画像を挿入説明
2.ブレークポイント254 ServerBootstrapAcceptor
ここに画像を挿入説明
childGroupその労働者EventLoopGroup

childGroup.register(子)へ

public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

スレッド内のレジスタワーカーeventLoopGroup後にイベントループを開始します

変更点観測スレッド
より良い観測のために、次のように2 EventLoopGroupでwelcomeServerを読みます

EventLoopGroup bossGroup = new NioEventLoopGroup(0,new DefaultThreadFactory("boss"));
EventLoopGroup workerGroup = new NioEventLoopGroup(0,new DefaultThreadFactory("worker"));      

完全なサーバの起動には、一つだけボス-1-1がある
ここに画像を挿入説明
のtelnet 127.0.0.1 8888の後、あなたは1つの以上のワーカー-3-1を参照することができ
ここに画像を挿入説明
、その後、CMDは、telnet 127.0.0.1 8888開き
ここに画像を挿入説明
、次のように網状スレッドモデルを
ここに画像を挿入説明

概要

中()メソッドを実行するための最初の実行

  1. デフォルトのswichに来る0を返さなければなりませんhasTask()が真であるが、selector.selectNow()ので、タスクキューがあり、サーバータスクを登録しました
  2. I / Oイベント、すなわち処理方法processSelectedKeys()を実行し、非I / O処理のイベントを実行する、すなわちrurunAllTask​​s(長いtimeoutNanos)
  3. runAllTask​​s(ロングtimeoutNanos)タスクキューセレクタに登録されたタスクを登録する作業に取りに行くために。そのようなのServerSocketChannelは、セレクタに登録します。
  4. telnet 127.0.0.1 8888のSocketChannel接続が存在するであろう、セレクタ中)(selectNowでこのSelectKeyに取得します
  5. 登録接続を処理するServerBootstrapAcceptor.channelReadにおけるパイプラインのNioServerSocketChannel
  6. 登録プロセスは、これは同じであるchildGroupに登録されたSocketChannel、登録プロセスとNioServerSocketChannelあります。

バインドに左

bindメソッド

bindメソッドは、比較的簡単です

通常のNIOサーバーの登録コードは次のようです

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()
        Selector selector = Selector.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

しかし、実際には網状であります

 private ChannelFuture doBind(final SocketAddress localAddress) {
         //1.先注册channel到selector上
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

       //2.如果注册成功了
        if (regFuture.isDone()) {
            ChannelPromise promise = channel.newPromise();
        //3.再绑定到localAddress
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
           ...
        }
    }

doBind0このメソッドは、チャネルがをlocalAddressにバインドされ、比較的簡単です。この方法は、直接結合されていないことに注意してくださいが、非I / Oなどのタスクは、タスクキュー年に加えます。
これは、呼び出し元のスレッドスタックを次の
ここに画像を挿入説明

次のコード

protected void doBeginRead() throws Exception {
        // Channel.read() or ChannelHandlerContext.read() was called
        final SelectionKey selectionKey = this.selectionKey;
        if (!selectionKey.isValid()) {
            return;
        }

        readPending = true;

        final int interestOps = selectionKey.interestOps();
        if ((interestOps & readInterestOp) == 0) {
            selectionKey.interestOps(interestOps | readInterestOp);
        }
    }

それはスタートが0に設定されている理由を説明したが、後に事実となり、SelectionKey.OP_ACCEPTを設定します。

//为什么这里是0而是SelectionKey.OP_ACCEPT?
//在AbstractNioChannel.doBeginRead里会改,这个bind里再分析
 protected void doRegister() throws Exception {
   selectionKey = javaChannel().register(eventLoop().selector, 0, this);
   }

おすすめ

転載: blog.csdn.net/guo_xl/article/details/86671309