public class MethodChannel {
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  //......
  private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
  }
}
public final class BasicMessageChannel<T> {
  @NonNull private final BinaryMessenger messenger;
  @NonNull private final String name;
  @NonNull private final MessageCodec<T> codec;
  //......
  private final class IncomingMessageHandler implements BinaryMessageHandler {
    private final MessageHandler<T> handler;
  }
}
public final class EventChannel {
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  //......
  private final class IncomingStreamRequestHandler implements BinaryMessageHandler {
    private final StreamHandler handler;
  }
}MethodChannel、BasicMessageChannel、EventChannel 结构来看,都是由name,messenger和codec三个属性字段组成
name:String 类型,唯一标识符代表 Channel 的名字,因为一个 Flutter 应用中存在多个                         Channel,每个 Channel 在创建时必须指定一个独一无二的 name 作为标识
messenger:BinaryMessenger 类型,充当信使邮递员角色,消息的发送与接收工具人。
codec:MethodCodec 或MessageCodec<T>类型,充当消息的编解码器。
重点逻辑是messenger字段,看下BinaryMessenger定义
public interface BinaryMessenger {
   public interface TaskQueue {}
  @UiThread
  void send(@NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryReply callback);
  @UiThread
  void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler);
  interface BinaryMessageHandler {
  
    @UiThread
    void onMessage(@Nullable ByteBuffer message, @NonNull BinaryReply reply);
  }
   */
  interface BinaryReply {
   
    void reply(@Nullable ByteBuffer reply);
  }
}看下DartMessenger实现类
class DartMessenger implements BinaryMessenger, PlatformMessageHandler {
  @NonNull private final Map<String, HandlerInfo> messageHandlers = new HashMap<>();
  
@Override
  public void setMessageHandler(
      @NonNull String channel,
      @Nullable BinaryMessenger.BinaryMessageHandler handler,
      @Nullable TaskQueue taskQueue) {
    ......
    List<BufferedMessageInfo> list;
    synchronized (handlersLock) {
      messageHandlers.put(channel, new HandlerInfo(handler, dartMessengerTaskQueue));
    ......
    }
    ......
    }
  }
  @Override
  public void handleMessageFromDart(
      @NonNull String channel, @Nullable ByteBuffer message, int replyId, long messageData) {
    synchronized (handlersLock) {
      handlerInfo = messageHandlers.get(channel);
      messageDeferred = (enableBufferingIncomingMessages.get() && handlerInfo == null);
     ......
    if (!messageDeferred) {
      dispatchMessageToQueue(channel, handlerInfo, message, replyId, messageData);
    }
  }
  private void dispatchMessageToQueue(
      @NonNull String channel,
      @Nullable HandlerInfo handlerInfo,
      @Nullable ByteBuffer message,
      int replyId,
      long messageData) {
    // Called from any thread.
    final DartMessengerTaskQueue taskQueue = (handlerInfo != null) ? handlerInfo.taskQueue : null;
    TraceSection.beginAsyncSection("PlatformChannel ScheduleHandler on " + channel, replyId);
    Runnable myRunnable =
        () -> {
          TraceSection.endAsyncSection("PlatformChannel ScheduleHandler on " + channel, replyId);
          try (TraceSection e =
              TraceSection.scoped("DartMessenger#handleMessageFromDart on " + channel)) {
            invokeHandler(handlerInfo, message, replyId);
            if (message != null && message.isDirect()) {
              // This ensures that if a user retains an instance to the ByteBuffer and it
              // happens to be direct they will get a deterministic error.
              message.limit(0);
            }
          } finally {
            // This is deleting the data underneath the message object.
            flutterJNI.cleanupMessageData(messageData);
          }
        };
    final DartMessengerTaskQueue nonnullTaskQueue =
        taskQueue == null ? platformTaskQueue : taskQueue;
    nonnullTaskQueue.dispatch(myRunnable);
  }
  private void invokeHandler(
      @Nullable HandlerInfo handlerInfo, @Nullable ByteBuffer message, final int replyId) {
    // Called from any thread.
    if (handlerInfo != null) {
      try {
        Log.v(TAG, "Deferring to registered handler to process message.");
        handlerInfo.handler.onMessage(message, new Reply(flutterJNI, replyId));
      } catch (Exception ex) {
        Log.e(TAG, "Uncaught exception in binary message listener", ex);
        flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
      } catch (Error err) {
        handleError(err);
      }
    } else {
      Log.v(TAG, "No registered handler for message. Responding to Dart with empty reply message.");
      flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
    }
  }
}上面整体流程先注册handler,以channel name为key缓存到messageHandlers,等待
handleMessageFromDart(String channel, ByteBuffer message, int replyId, long messageData)
从Flutter端收到数据后,根据channel参数从messageHandlers获取到对应HandlerInfo对象,然后执行该队向的onMessage方法 handlerInfo.handler.onMessage(message, new Reply(flutterJNI, replyId));
这里可以先看下Reply对象,创建Reply对象保存replyId字段,后面用来回复Flutter。
  static class Reply implements BinaryMessenger.BinaryReply {
    @NonNull private final FlutterJNI flutterJNI;
    private final int replyId;
    private final AtomicBoolean done = new AtomicBoolean(false);
    Reply(@NonNull FlutterJNI flutterJNI, int replyId) {
      this.flutterJNI = flutterJNI;
      this.replyId = replyId;
    }
    @Override
    public void reply(@Nullable ByteBuffer reply) {
      if (done.getAndSet(true)) {
        throw new IllegalStateException("Reply already submitted");
      }
      if (reply == null) {
        flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
      } else {
        flutterJNI.invokePlatformMessageResponseCallback(replyId, reply, reply.position());
      }
    }
  }通过flutterJNI调用invokePlatformMessageResponseCallback发送到Flutter端
现在回过头继续跟踪setMessageHandler流程,前面提到channel有MethodChannel,EventChannel和BasicMessageChannel,分别看下它们是如何注册handler的,三种类型有啥不一样。
先看MethodChannel类型
public class MethodChannel {
  private static final String TAG = "MethodChannel#";
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  private final BinaryMessenger.TaskQueue taskQueue;
 ......
  @UiThread
  public void setMethodCallHandler(final @Nullable MethodCallHandler handler) {
    // We call the 2 parameter variant specifically to avoid breaking changes in
    // mock verify calls.
    // See https://github.com/flutter/flutter/issues/92582.
    if (taskQueue != null) {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingMethodCallHandler(handler), taskQueue);
    } else {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingMethodCallHandler(handler));
    }
  }
......
}注册的类型为IncomingMethodCallHandler,从Incoming名字看就是“接听”,用来处理Flutter端发送的数据。
  private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
    IncomingMethodCallHandler(MethodCallHandler handler) {
      this.handler = handler;
    }
    @Override
    @UiThread
    public void onMessage(ByteBuffer message, final BinaryReply reply) {
      final MethodCall call = codec.decodeMethodCall(message);
      try {
        handler.onMethodCall(
            call,
            new Result() {
              @Override
              public void success(Object result) {
                reply.reply(codec.encodeSuccessEnvelope(result));
              }
              @Override
              public void error(String errorCode, String errorMessage, Object errorDetails) {
                reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
              }
              @Override
              public void notImplemented() {
                reply.reply(null);
              }
            });
      } catch (RuntimeException e) {
        Log.e(TAG + name, "Failed to handle method call", e);
        reply.reply(
            codec.encodeErrorEnvelopeWithStacktrace(
                "error", e.getMessage(), null, Log.getStackTraceString(e)));
      }
    }
  }onMessage方法通过codec编码器将字节数组message转化成MethodCall对象,MethodCall对象里面包括方法名称和方法携带参数
再看EventChannel类型
public final class EventChannel {
  private static final String TAG = "EventChannel#";
  private final BinaryMessenger messenger;
  private final String name;
  private final MethodCodec codec;
  @Nullable private final BinaryMessenger.TaskQueue taskQueue;
  ......
  public void setStreamHandler(final StreamHandler handler) {
    // We call the 2 parameter variant specifically to avoid breaking changes in
    // mock verify calls.
    // See https://github.com/flutter/flutter/issues/92582.
    if (taskQueue != null) {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingStreamRequestHandler(handler), taskQueue);
    } else {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingStreamRequestHandler(handler));
    }
  }
......
}注册的类型为IncomingStreamRequestHandler,从Incoming名字看也是带“接听”,同样是用来处理Flutter端发送的数据。
 private final class IncomingStreamRequestHandler implements BinaryMessageHandler {
    private final StreamHandler handler;
    private final AtomicReference<EventSink> activeSink = new AtomicReference<>(null);
    IncomingStreamRequestHandler(StreamHandler handler) {
      this.handler = handler;
    }
    @Override
    public void onMessage(ByteBuffer message, final BinaryReply reply) {
      final MethodCall call = codec.decodeMethodCall(message);
      if (call.method.equals("listen")) {
        onListen(call.arguments, reply);
      } else if (call.method.equals("cancel")) {
        onCancel(call.arguments, reply);
      } else {
        reply.reply(null);
      }
    }
}IncomingStreamRequestHandler 的onMessage方法通过使用codec编码器将字节数组转成MethodCall对象,可以看到该对象只有listen和cancel两个方法,listen标识监听,cancel标识取消监听
最后再看BasicMessageChannel
public final class BasicMessageChannel<T> {
  private static final String TAG = "BasicMessageChannel#";
  public static final String CHANNEL_BUFFERS_CHANNEL = "dev.flutter/channel-buffers";
  @NonNull private final BinaryMessenger messenger;
  @NonNull private final String name;
  @NonNull private final MessageCodec<T> codec;
  @Nullable private final BinaryMessenger.TaskQueue taskQueue;
......
  public void setMessageHandler(@Nullable final MessageHandler<T> handler) {
    // We call the 2 parameter variant specifically to avoid breaking changes in
    // mock verify calls.
    // See https://github.com/flutter/flutter/issues/92582.
    if (taskQueue != null) {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingMessageHandler(handler), taskQueue);
    } else {
      messenger.setMessageHandler(
          name, handler == null ? null : new IncomingMessageHandler(handler));
    }
  }
......
}注册的类型为IncomingMessageHandler,名字依然带Incoming,同样是用来处理Flutter端发送的数据。
  private final class IncomingMessageHandler implements BinaryMessageHandler {
    private final MessageHandler<T> handler;
    private IncomingMessageHandler(@NonNull MessageHandler<T> handler) {
      this.handler = handler;
    }
    @Override
    public void onMessage(@Nullable ByteBuffer message, @NonNull final BinaryReply callback) {
      try {
        handler.onMessage(
            codec.decodeMessage(message),
            new Reply<T>() {
              @Override
              public void reply(T reply) {
                callback.reply(codec.encodeMessage(reply));
              }
            });
      } catch (RuntimeException e) {
        Log.e(TAG + name, "Failed to handle message", e);
        callback.reply(null);
      }
    }
  }onMessage方法里面将message通过codec编码器转成T对应类型。
通过上面的代码跟踪,我们已经明了MethodChannel注册到messageHandlers里面的是IncomingMethodCallHandler,EventChannel注册的是IncomingStreamRequestHandler,BasicMessageChannel注册的是IncomingMessageHandler
| MethodChannel | IncomingMethodCallHandler | 
| EventChannel | IncomingStreamRequestHandler | 
| BasicMessageChannel | IncomingMessageHandler | 
我们继续跟踪IncomingMethodCallHandler,确认它是如何处理Flutter的方法调用
我们以官方的MouseCursorChannel为例,确认逻辑细节
public class MouseCursorChannel {
  private static final String TAG = "MouseCursorChannel";
  @NonNull public final MethodChannel channel;
  @Nullable private MouseCursorMethodHandler mouseCursorMethodHandler;
  public MouseCursorChannel(@NonNull DartExecutor dartExecutor) {
    channel = new MethodChannel(dartExecutor, "flutter/mousecursor", StandardMethodCodec.INSTANCE);
    channel.setMethodCallHandler(parsingMethodCallHandler);
  }
  /**
   * Sets the {@link MouseCursorMethodHandler} which receives all events and requests that are
   * parsed from the underlying platform channel.
   */
  public void setMethodHandler(@Nullable MouseCursorMethodHandler mouseCursorMethodHandler) {
    this.mouseCursorMethodHandler = mouseCursorMethodHandler;
  }
  @NonNull
  private final MethodChannel.MethodCallHandler parsingMethodCallHandler =
      new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          if (mouseCursorMethodHandler == null) {
            // If no explicit mouseCursorMethodHandler has been registered then we don't
            // need to forward this call to an API. Return.
            return;
          }
          final String method = call.method;
          Log.v(TAG, "Received '" + method + "' message.");
          try {
            // More methods are expected to be added here, hence the switch.
            switch (method) {
              case "activateSystemCursor":
                @SuppressWarnings("unchecked")
                final HashMap<String, Object> data = (HashMap<String, Object>) call.arguments;
                final String kind = (String) data.get("kind");
                try {
                  mouseCursorMethodHandler.activateSystemCursor(kind);
                } catch (Exception e) {
                  result.error("error", "Error when setting cursors: " + e.getMessage(), null);
                  break;
                }
                result.success(true);
                break;
              default:
            }
          } catch (Exception e) {
            result.error("error", "Unhandled error: " + e.getMessage(), null);
          }
        }
      };
  @VisibleForTesting
  public void synthesizeMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
    parsingMethodCallHandler.onMethodCall(call, result);
  }
  public interface MouseCursorMethodHandler {
    // Called when the pointer should start displaying a system mouse cursor
    // specified by {@code shapeCode}.
    public void activateSystemCursor(@NonNull String kind);
  }
}可以看到,真正干活的handler就是parsingMethodCallHandler,它里面复写了onMethodCall方法,从源码可以看出支持方法名activateSystemCursor,执行完成后通过result.success(true)回调回去,这里的result就是前面的Result
    public void onMessage(ByteBuffer message, final BinaryReply reply) {
      final MethodCall call = codec.decodeMethodCall(message);
      try {
        handler.onMethodCall(
            call,
            new Result() {
              @Override
              public void success(Object result) {
                reply.reply(codec.encodeSuccessEnvelope(result));
              }
              @Override
              public void error(String errorCode, String errorMessage, Object errorDetails) {
                reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails));
              }
              @Override
              public void notImplemented() {
                reply.reply(null);
              }
            });
      } catch (RuntimeException e) {
        Log.e(TAG + name, "Failed to handle method call", e);
        reply.reply(
            codec.encodeErrorEnvelopeWithStacktrace(
                "error", e.getMessage(), null, Log.getStackTraceString(e)));
      }
    }result接收到success回调后,通过reply.reply 回调给Flutter层。
这样就明确了,Android从Flutter端收到数据后,执行完对应方法后,再通过reply通过FlutterJNI发送到Flutter,这样一个调用回路就形成了。
EventChannel和BasicMessageChannel逻辑也是类似,这里就不再赘述,大家可以查阅源码进行分析。
上面的整个流程都是Flutter调用Android原生的方法流程,接下来我们跟踪下Android如何Flutter的方法
我们以NavigationChannel为例,看看它是如何调用的
  public void pushRoute(@NonNull String route) {
    Log.v(TAG, "Sending message to push route '" + route + "'");
    channel.invokeMethod("pushRoute", route);
  }  @UiThread
  public void invokeMethod(@NonNull String method, @Nullable Object arguments) {
    invokeMethod(method, arguments, null);
  }  @UiThread
  public void invokeMethod(
      @NonNull String method, @Nullable Object arguments, @Nullable Result callback) {
    messenger.send(
        name,
        codec.encodeMethodCall(new MethodCall(method, arguments)),
        callback == null ? null : new IncomingResultHandler(callback));
  }  @Override
  public void send(
      @NonNull String channel,
      @Nullable ByteBuffer message,
      @Nullable BinaryMessenger.BinaryReply callback) {
    try (TraceSection e = TraceSection.scoped("DartMessenger#send on " + channel)) {
      Log.v(TAG, "Sending message with callback over channel '" + channel + "'");
      int replyId = nextReplyId++;
      if (callback != null) {
        pendingReplies.put(replyId, callback);
      }
      if (message == null) {
        flutterJNI.dispatchEmptyPlatformMessage(channel, replyId);
      } else {
        flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId);
      }
    }
  }跟踪源码,我们看到最后是通过flutterJNI调用了dispatchPlatformMessage方法发送到了Flutter层。
Android原生MethodChannel整体调用流程如下图

接下来我们根据下MethodChannel里面的codec编码器是如何工作的。
我们可以看到MethodChannel里面的codec是MethodCodec类型。
public interface MethodCodec {
  @NonNull
  ByteBuffer encodeMethodCall(@NonNull MethodCall methodCall);
  @NonNull
  MethodCall decodeMethodCall(@NonNull ByteBuffer methodCall);
  @NonNull
  ByteBuffer encodeSuccessEnvelope(@Nullable Object result);
  @NonNull
  ByteBuffer encodeErrorEnvelope(
      @NonNull String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails);
  @NonNull
  ByteBuffer encodeErrorEnvelopeWithStacktrace(
      @NonNull String errorCode,
      @Nullable String errorMessage,
      @Nullable Object errorDetails,
      @Nullable String errorStacktrace);
  @NonNull
  Object decodeEnvelope(@NonNull ByteBuffer envelope);
}这里一般使用的是StandardMethodCodec类型。
public final class StandardMethodCodec implements MethodCodec {
  public static final StandardMethodCodec INSTANCE =
      new StandardMethodCodec(StandardMessageCodec.INSTANCE);
  private final StandardMessageCodec messageCodec;
  /** Creates a new method codec based on the specified message codec. */
  public StandardMethodCodec(@NonNull StandardMessageCodec messageCodec) {
    this.messageCodec = messageCodec;
  }
  @Override
  @NonNull
  public ByteBuffer encodeMethodCall(@NonNull MethodCall methodCall) {
    final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
    messageCodec.writeValue(stream, methodCall.method);
    messageCodec.writeValue(stream, methodCall.arguments);
    final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
    buffer.put(stream.buffer(), 0, stream.size());
    return buffer;
  }
  @Override
  @NonNull
  public MethodCall decodeMethodCall(@NonNull ByteBuffer methodCall) {
    methodCall.order(ByteOrder.nativeOrder());
    final Object method = messageCodec.readValue(methodCall);
    final Object arguments = messageCodec.readValue(methodCall);
    if (method instanceof String && !methodCall.hasRemaining()) {
      return new MethodCall((String) method, arguments);
    }
    throw new IllegalArgumentException("Method call corrupted");
  }
}可以看到encodeMethodCall将MethodCall对象编码成字节数组,decodeMethodCall将字节数组转化成MethodCall对象。这里的核心算法是通过messageCodec writeValue和readValue转化。
  protected void writeValue(@NonNull ByteArrayOutputStream stream, @Nullable Object value) {
    if (value == null || value.equals(null)) {
      stream.write(NULL);
    } else if (value instanceof Boolean) {
      stream.write(((Boolean) value).booleanValue() ? TRUE : FALSE);
    } else if (value instanceof Number) {
      if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
        stream.write(INT);
        writeInt(stream, ((Number) value).intValue());
      } else if (value instanceof Long) {
        stream.write(LONG);
        writeLong(stream, (long) value);
      } else if (value instanceof Float || value instanceof Double) {
        stream.write(DOUBLE);
        writeAlignment(stream, 8);
        writeDouble(stream, ((Number) value).doubleValue());
      } else if (value instanceof BigInteger) {
        stream.write(BIGINT);
        writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
      } else {
        throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());
      }
    } else if (value instanceof CharSequence) {
      stream.write(STRING);
      writeBytes(stream, value.toString().getBytes(UTF8));
    } else if (value instanceof byte[]) {
      stream.write(BYTE_ARRAY);
      writeBytes(stream, (byte[]) value);
    } else if (value instanceof int[]) {
      stream.write(INT_ARRAY);
      final int[] array = (int[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 4);
      for (final int n : array) {
        writeInt(stream, n);
      }
    } else if (value instanceof long[]) {
      stream.write(LONG_ARRAY);
      final long[] array = (long[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 8);
      for (final long n : array) {
        writeLong(stream, n);
      }
    } else if (value instanceof double[]) {
      stream.write(DOUBLE_ARRAY);
      final double[] array = (double[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 8);
      for (final double d : array) {
        writeDouble(stream, d);
      }
    } else if (value instanceof List) {
      stream.write(LIST);
      final List<?> list = (List) value;
      writeSize(stream, list.size());
      for (final Object o : list) {
        writeValue(stream, o);
      }
    } else if (value instanceof Map) {
      stream.write(MAP);
      final Map<?, ?> map = (Map) value;
      writeSize(stream, map.size());
      for (final Entry<?, ?> entry : map.entrySet()) {
        writeValue(stream, entry.getKey());
        writeValue(stream, entry.getValue());
      }
    } else if (value instanceof float[]) {
      stream.write(FLOAT_ARRAY);
      final float[] array = (float[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 4);
      for (final float f : array) {
        writeFloat(stream, f);
      }
    } else {
      throw new IllegalArgumentException(
          "Unsupported value: '" + value + "' of type '" + value.getClass() + "'");
    }
  }可以看到写入到字节流时,先写入类型,再写入数据,类型固定占一个字节。
  private static final byte NULL = 0;
  private static final byte TRUE = 1;
  private static final byte FALSE = 2;
  private static final byte INT = 3;
  private static final byte LONG = 4;
  private static final byte BIGINT = 5;
  private static final byte DOUBLE = 6;
  private static final byte STRING = 7;
  private static final byte BYTE_ARRAY = 8;
  private static final byte INT_ARRAY = 9;
  private static final byte LONG_ARRAY = 10;
  private static final byte DOUBLE_ARRAY = 11;
  private static final byte LIST = 12;
  private static final byte MAP = 13;
  private static final byte FLOAT_ARRAY = 14;我们追踪下字符串如何写入
if (value instanceof CharSequence) {
      stream.write(STRING);
      writeBytes(stream, value.toString().getBytes(UTF8));
    } 
-------------------------------------------------------------------------
  protected static final void writeBytes(
      @NonNull ByteArrayOutputStream stream, @NonNull byte[] bytes) {
    writeSize(stream, bytes.length);
    stream.write(bytes, 0, bytes.length);
  }先写入STRING类型,再写入字符串的长度,再写入字符串内容。
现在我们已经明确了,通过类型+(数据长度)+数据就能将方法进行编码,响应的我们根据这个规则就可以将字节数据进行解码,获得方法名和参数类型。
| 类型 | 数据长度 | 数据 | ...... | 
从上面也能看到,这里不支持自定义类型,只支持普通数据类型。













![【PostgreSQL17新特性之-冗余IS [NOT] NULL限定符的处理优化】](https://img-blog.csdnimg.cn/img_convert/d8c52000ffd33b61ebaa1a0113f46d9c.png)





![[代码复现]Self-Attentive Sequential Recommendation(ing)](https://img-blog.csdnimg.cn/direct/196fac48d6b848fba9abf60f8c7b44bd.png)