AOSP15 Input专题InputDispatcher源码分析

InputReader 依然是输入系统(Input System)的核心组件,负责从内核读取原始事件数据。需要将其放在 InputDispatcher 进行派发处理。

一、 核心流程概览

  1. 系统事件流程
c++ 复制代码
InputDispatcher::dispatchOnce() 
  -> dispatchOnceInnerLocked() 
    -> dispatchMotionLocked() // 处理 Motion 事件
      -> findTouchedWindowTargetsLocked(currentTime, motionEntry, inputTargets, nextWakeupTime) //计算目标窗口 (核心算法),该函数决定了事件最终去向哪里窗口
      -> dispatchEventLocked(nsecs_t currentTime,std::shared_ptr<const EventEntry> eventEntry,const std::vector<InputTarget>& inputTargets) 
        -> prepareDispatchCycleLocked(nsecs_t currentTime,const std::shared_ptr<Connection>& connection,std::shared_ptr<const EventEntry> eventEntry,const InputTarget& inputTarget) 
          -> enqueueDispatchEntryAndStartDispatchCycleLocked(nsecs_t currentTime, const std::shared_ptr<Connection>& connection,std::shared_ptr<const EventEntry> eventEntry, const InputTarget& inputTarget) 
            -> enqueueDispatchEntryLocked(const std::shared_ptr<Connection>& connection,std::shared_ptr<const EventEntry> eventEntry,const InputTarget& inputTarget)//构建DispatchEntry,将事件封装进每个目标窗口的 outboundQueue
            -> startDispatchCycleLocked(nsecs_t currentTime,const std::shared_ptr<Connection>& connection)//实际执行 Socket 发送
  1. 应用层接收反馈
c++ 复制代码
InputDispatcher
  -> InputChannel
     -> WindowInputEventReceiver
         -> ViewRootImpl.enqueueInputEvent
            -> ViewRootImpl.doProcessInputEvents
               -> deliverInputEvent
                  -> InputStage pipeline
                     -> View.dispatchTouchEvent
                        -> View.onTouchEvent
                           -> finishInputEvent

二、 详细调用链路分析

1. dispatchOnce(): 主循环入口

c++ 复制代码
void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LLONG_MAX;
    { 
        std::scoped_lock _l(mLock);
        mDispatcherIsAlive.notify_all();

        // 省略代码
        if (!haveCommandsLocked()) {
            dispatchOnceInnerLocked(/*byref*/ nextWakeupTime);
        }

        // 省略代码
    } 
    // 省略代码
}

2. dispatchOnceInnerLocked(): 事件分发的核心逻辑所在

c++ 复制代码
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // [步骤 1] 检查 ANR: 在派发新事件前,必须先处理超时的连接
    processAnrsLocked();

    // [步骤 2] 获取并处理队列中的事件 (mPendingEvent)
    if (mPendingEvent) {
        // ... (省略部分逻辑) ...
        // [步骤 3] 分发 Motion 事件
        if (dispatchMotionLocked(currentTime, static_cast<MotionEntry*>(mPendingEvent), ...)) {
            // ...
        }
    }
}

3. dispatchMotionLocked() (以触摸事件为例):

c++ 复制代码
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime,
                                           std::shared_ptr<const MotionEntry> entry,
                                           DropReason* dropReason, nsecs_t& nextWakeupTime) {
    // 省略代码
    if (isPointerEvent) {
       // 省略代码

        // 根据当前所有窗口的层级 (Z-order)、坐标范围、以及 WindowInfo 的标志位(如 FLAG_NOT_TOUCH_MODAL)来计算谁该接收这个触摸
        Result<std::vector<InputTarget>, InputEventInjectionResult> result =
                findTouchedWindowTargetsLocked(currentTime, *entry);

        // 省略代码
    } else {
       // 省略代码
        addWindowTargetLocked(focusedWindow, InputTarget::DispatchMode::AS_IS,
                                  InputTarget::Flags::FOREGROUND, getDownTime(*entry),
                                  inputTargets);
        // 省略代码
    }
    // 省略代码
    // 开始处理DispatchEntry数据
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

4. dispatchEventLocked()

c++ 复制代码
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
                                          std::shared_ptr<const EventEntry> eventEntry,
                                          const std::vector<InputTarget>& inputTargets) {
    ATRACE_CALL();
    // 省略代码
    for (const InputTarget& inputTarget : inputTargets) {
        std::shared_ptr<Connection> connection = inputTarget.connection;
        prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);
    }
}

5. prepareDispatchCycleLocked()

c++ 复制代码
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
                                                 const std::shared_ptr<Connection>& connection,
                                                 std::shared_ptr<const EventEntry> eventEntry,
                                                 const InputTarget& inputTarget) {
     // 省略代码
    if (connection->status != Connection::Status::NORMAL) {
        if (DEBUG_DISPATCH_CYCLE) {
            ALOGD("channel '%s' ~ Dropping event because the channel status is %s",
                  connection->getInputChannelName().c_str(),
                  ftl::enum_string(connection->status).c_str());
        }
        return;
    }

    // 分屏split
    if (inputTarget.flags.test(InputTarget::Flags::SPLIT)) {

        const MotionEntry& originalMotionEntry = static_cast<const MotionEntry&>(*eventEntry);
        if (inputTarget.getPointerIds().count() != originalMotionEntry.getPointerCount()) {
            // 省略代码
            enqueueDispatchEntryAndStartDispatchCycleLocked(currentTime, connection,
                                                            std::move(splitMotionEntry),
                                                            inputTarget);
            return;
        }
    }
    // 单屏
    enqueueDispatchEntryAndStartDispatchCycleLocked(currentTime, connection, eventEntry,
                                                    inputTarget);
}

6. enqueueDispatchEntryAndStartDispatchCycleLockeds()

c++ 复制代码
void InputDispatcher::enqueueDispatchEntryAndStartDispatchCycleLocked(
        nsecs_t currentTime, const std::shared_ptr<Connection>& connection,
        std::shared_ptr<const EventEntry> eventEntry, const InputTarget& inputTarget) {
   
    // 构建DispatchEntry,将事件封装进每个目标窗口的 
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget);

    // 发送分发消息给应用
    if (wasEmpty && !connection->outboundQueue.empty()) {
        startDispatchCycleLocked(currentTime, connection);
    }
}

7. enqueueDispatchEntryLocked()

构建DispatchEntry,塞入outboundQueue

c++ 复制代码
void InputDispatcher::enqueueDispatchEntryLocked(const std::shared_ptr<Connection>& connection,
                                                 std::shared_ptr<const EventEntry> eventEntry,
                                                 const InputTarget& inputTarget) {
    // 省略代码
    eventEntry = dispatchEntry->eventEntry;
    switch (eventEntry->type) {
        // 省略代码
        case EventEntry::Type::MOTION: {
            std::shared_ptr<const MotionEntry> resolvedMotion =
                    std::static_pointer_cast<const MotionEntry>(eventEntry);
            {
                const MotionEntry& motionEntry = static_cast<const MotionEntry&>(*eventEntry);
                int32_t resolvedAction = motionEntry.action;
                int32_t resolvedFlags = motionEntry.flags;

                if (inputTarget.dispatchMode == InputTarget::DispatchMode::OUTSIDE) {
                    resolvedAction = AMOTION_EVENT_ACTION_OUTSIDE;
                } else if (inputTarget.dispatchMode == InputTarget::DispatchMode::HOVER_EXIT) {
                    resolvedAction = AMOTION_EVENT_ACTION_HOVER_EXIT;
                } else if (inputTarget.dispatchMode == InputTarget::DispatchMode::HOVER_ENTER) {
                    resolvedAction = AMOTION_EVENT_ACTION_HOVER_ENTER;
                } else if (inputTarget.dispatchMode == InputTarget::DispatchMode::SLIPPERY_EXIT) {
                    resolvedAction = AMOTION_EVENT_ACTION_CANCEL;
                } else if (inputTarget.dispatchMode == InputTarget::DispatchMode::SLIPPERY_ENTER) {
                    resolvedAction = AMOTION_EVENT_ACTION_DOWN;
                }
                if (resolvedAction == AMOTION_EVENT_ACTION_HOVER_MOVE &&
                    !connection->inputState.isHovering(motionEntry.deviceId, motionEntry.source,
                                                       motionEntry.displayId)) {
                    if (DEBUG_DISPATCH_CYCLE) {
                        LOG(DEBUG) << "channel '" << connection->getInputChannelName().c_str()
                                   << "' ~ enqueueDispatchEntryLocked: filling in missing hover "
                                      "enter event";
                    }
                    resolvedAction = AMOTION_EVENT_ACTION_HOVER_ENTER;
                }

                if (resolvedAction == AMOTION_EVENT_ACTION_CANCEL) {
                    resolvedFlags |= AMOTION_EVENT_FLAG_CANCELED;
                }
                if (dispatchEntry->targetFlags.test(InputTarget::Flags::WINDOW_IS_OBSCURED)) {
                    resolvedFlags |= AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED;
                }
                if (dispatchEntry->targetFlags.test(
                            InputTarget::Flags::WINDOW_IS_PARTIALLY_OBSCURED)) {
                    resolvedFlags |= AMOTION_EVENT_FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
                }
                if (dispatchEntry->targetFlags.test(InputTarget::Flags::NO_FOCUS_CHANGE)) {
                    resolvedFlags |= AMOTION_EVENT_FLAG_NO_FOCUS_CHANGE;
                }

                dispatchEntry->resolvedFlags = resolvedFlags;
                if (resolvedAction != motionEntry.action) {
                    std::optional<std::vector<PointerProperties>> usingProperties;
                    std::optional<std::vector<PointerCoords>> usingCoords;
                    if (resolvedAction == AMOTION_EVENT_ACTION_HOVER_EXIT ||
                        resolvedAction == AMOTION_EVENT_ACTION_CANCEL) {
                        const bool hovering = resolvedAction == AMOTION_EVENT_ACTION_HOVER_EXIT;
                        std::optional<std::pair<std::vector<PointerProperties>,
                                                std::vector<PointerCoords>>>
                                pointerInfo =
                                        connection->inputState.getPointersOfLastEvent(motionEntry,
                                                                                      hovering);
                        if (pointerInfo) {
                            usingProperties = pointerInfo->first;
                            usingCoords = pointerInfo->second;
                        }
                    }
                    {
                        // Generate a new MotionEntry with a new eventId using the resolved action
                        // and flags, and set it as the resolved entry.
                        auto newEntry = std::make_shared<
                                MotionEntry>(mIdGenerator.nextId(), motionEntry.injectionState,
                                             motionEntry.eventTime, motionEntry.deviceId,
                                             motionEntry.source, motionEntry.displayId,
                                             motionEntry.policyFlags, resolvedAction,
                                             motionEntry.actionButton, resolvedFlags,
                                             motionEntry.metaState, motionEntry.buttonState,
                                             motionEntry.classification, motionEntry.edgeFlags,
                                             motionEntry.xPrecision, motionEntry.yPrecision,
                                             motionEntry.xCursorPosition,
                                             motionEntry.yCursorPosition, motionEntry.downTime,
                                             usingProperties.value_or(
                                                     motionEntry.pointerProperties),
                                             usingCoords.value_or(motionEntry.pointerCoords));
                        if (mTracer) {
                            ensureEventTraced(motionEntry);
                            newEntry->traceTracker =
                                    mTracer->traceDerivedEvent(*newEntry,
                                                               *motionEntry.traceTracker);
                        }
                        resolvedMotion = newEntry;
                    }
                    if (ATRACE_ENABLED()) {
                        std::string message = StringPrintf("Transmute MotionEvent(id=0x%" PRIx32
                                                           ") to MotionEvent(id=0x%" PRIx32 ").",
                                                           motionEntry.id, resolvedMotion->id);
                        ATRACE_NAME(message.c_str());
                    }
                    dispatchEntry->eventEntry = resolvedMotion;
                    eventEntry = resolvedMotion;
                }
            }
            std::unique_ptr<EventEntry> cancelEvent =
                    connection->inputState.cancelConflictingInputStream(*resolvedMotion);
            if (cancelEvent != nullptr) {
                LOG(INFO) << "Canceling pointers for device " << resolvedMotion->deviceId << " in "
                          << connection->getInputChannelName() << " with event "
                          << cancelEvent->getDescription();
                if (mTracer) {
                    static_cast<MotionEntry&>(*cancelEvent).traceTracker =
                            mTracer->traceDerivedEvent(*cancelEvent, *resolvedMotion->traceTracker);
                }
                std::unique_ptr<DispatchEntry> cancelDispatchEntry =
                        createDispatchEntry(mIdGenerator, inputTarget, std::move(cancelEvent),
                                            ftl::Flags<InputTarget::Flags>(), mWindowInfosVsyncId,
                                            mTracer.get());

                // 生成cancel事件,加入队列
                connection->outboundQueue.emplace_back(std::move(cancelDispatchEntry));
            }

            // 省略代码

            break;
        }
        // 省略代码
    }

    // 省略代码

    // 塞入waitQueue
    connection->outboundQueue.emplace_back(std::move(dispatchEntry));
    traceOutboundQueueLength(*connection);
}

8. startDispatchCycleLocked()

发送消息个应用侧

c++ 复制代码
oid InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
                                               const std::shared_ptr<Connection>& connection) {
    ATRACE_NAME_IF(ATRACE_ENABLED(),
                   StringPrintf("startDispatchCycleLocked(inputChannel=%s)",
                                connection->getInputChannelName().c_str()));
    if (DEBUG_DISPATCH_CYCLE) {
        ALOGD("channel '%s' ~ startDispatchCycle", connection->getInputChannelName().c_str());
    }

    while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
        std::unique_ptr<DispatchEntry>& dispatchEntry = connection->outboundQueue.front();
        dispatchEntry->deliveryTime = currentTime;
        const std::chrono::nanoseconds timeout = getDispatchingTimeoutLocked(connection);
        dispatchEntry->timeoutTime = currentTime + timeout.count();

        // Publish the event.
        status_t status;
        const EventEntry& eventEntry = *(dispatchEntry->eventEntry);
        switch (eventEntry.type) {
            // 省略代码
            case EventEntry::Type::MOTION: {
                if (DEBUG_OUTBOUND_EVENT_DETAILS) {
                    LOG(INFO) << "Publishing " << *dispatchEntry << " to "
                              << connection->getInputChannelName();
                }
                const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);
                // 发送sockets给应用侧
                status = publishMotionEvent(*connection, *dispatchEntry);
                if (status == BAD_VALUE) {
                    logDispatchStateLocked();
                    LOG(FATAL) << "Publisher failed for " << motionEntry;
                }
                if (mTracer) {
                    ensureEventTraced(motionEntry);
                    mTracer->traceEventDispatch(*dispatchEntry, *motionEntry.traceTracker);
                }
                break;
            }

           // 省略代码
        }

        // 省略代码

        const nsecs_t timeoutTime = dispatchEntry->timeoutTime;
        // 加入等待队列,用于判断ANR
        connection->waitQueue.emplace_back(std::move(dispatchEntry));
        //移除oq中数据
        connection->outboundQueue.erase(connection->outboundQueue.begin());
        traceOutboundQueueLength(*connection);
        if (connection->responsive) {
            // 加入等待队列,用于判断ANR
            mAnrTracker.insert(timeoutTime, connection->getToken());
        }
        traceWaitQueueLength(*connection);
    }
}

三、ANR的产生分析

1、ANR 主要分为 4 类:

类型 触发位置 超时时间
Input ANR InputDispatcher 默认 5s
Broadcast ANR AMS 前台 10s / 后台 60s
Service ANR AMS 20s
ContentProvider ANR AMS 10s

2、我们看最常碰到Input ANR

  • startDispatchCycleLocked()构建DispatchEntry时候加入waitQueuemAnrTracher用于判断超时:
c++ 复制代码
oid InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
                                               const std::shared_ptr<Connection>& connection) {
    ATRACE_NAME_IF(ATRACE_ENABLED(),
                   StringPrintf("startDispatchCycleLocked(inputChannel=%s)",
                                connection->getInputChannelName().c_str()));
    if (DEBUG_DISPATCH_CYCLE) {
        ALOGD("channel '%s' ~ startDispatchCycle", connection->getInputChannelName().c_str());
    }

    while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
        std::unique_ptr<DispatchEntry>& dispatchEntry = connection->outboundQueue.front();
        // 省略代码
        const EventEntry& eventEntry = *(dispatchEntry->eventEntry);
        switch (eventEntry.type) {
            // 省略代码
            case EventEntry::Type::MOTION: {
                // 省略代码
                const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);
                // 发送sockets给应用侧
                status = publishMotionEvent(*connection, *dispatchEntry);
                // 省略代码
                break;
            }

           // 省略代码
        }

        // 省略代码
        const nsecs_t timeoutTime = dispatchEntry->timeoutTime;
        // 加入等待队列,用于判断ANR
        connection->waitQueue.emplace_back(std::move(dispatchEntry));
        //移除oq中数据
        connection->outboundQueue.erase(connection->outboundQueue.begin());
        traceOutboundQueueLength(*connection);
        if (connection->responsive) {
            // 加入等待队列,用于判断ANR
            mAnrTracker.insert(timeoutTime, connection->getToken());
        }
        traceWaitQueueLength(*connection);
    }
}
  • processAnrsLocked()判断是否ANR

系统通过 mAnrTracker 维护每一个 Connection 的超时定时器。

返回值 含义
LLONG_MIN 立即重新循环(刚触发 ANR)
某个时间戳 下一次应检查 ANR 的时间
LLONG_MAX 当前没有任何需要检查的 ANR
c++ 复制代码
nsecs_t InputDispatcher::processAnrsLocked() {
    const nsecs_t currentTime = now();
    nsecs_t nextAnrCheck = LLONG_MAX;
    // 没焦点anr
    if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
        if (currentTime >= *mNoFocusedWindowTimeoutTime) {
            processNoFocusedWindowAnrLocked();
            mAwaitedFocusedApplication.reset();
            mNoFocusedWindowTimeoutTime = std::nullopt;
            return LLONG_MIN;
        } else {
            nextAnrCheck = *mNoFocusedWindowTimeoutTime;
        }
    }
    // 来获取最早的一个超时时间
    nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());
    // 没超时直接返回
    if (currentTime < nextAnrCheck) { 
        return nextAnrCheck;         
    }
    std::shared_ptr<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());
    // 没有等待的connection
    if (connection == nullptr) {
        return nextAnrCheck;
    }
    connection->responsive = false;
    // 从监控列表中移除该连接,防止重复触发。
    mAnrTracker.eraseToken(connection->getToken());
    // 触发ANR
    onAnrLocked(connection);
    return LLONG_MIN;
}
  • 应用侧接收 (Native 接入点) App 进程中的 NativeInputEventReceiver 通过 Looper 监听到 Socket 可读。
java 复制代码
// frameworks/native/android/native_input_event_receiver.cpp
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
    if (events & ALOOPER_EVENT_INPUT) {
        // 1. 从 Socket 读取数据
        // 2. 调用 JNI 方法通知 Java 层
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        env->CallVoidMethod(mReceiverObjGlobal.get(), gInputEventReceiverClassInfo.dispatchInputEvent, ...);
    }
    return 1; // Keep monitoring
}
  • Java 层调度 (ViewRootImpl 入口) 通过 JNI,事件进入 Java 层的 WindowInputEventReceiver。
java 复制代码
// frameworks/base/core/java/android/view/InputEventReceiver.java

// JNI 调用此方法
public void dispatchInputEvent(int seq, InputEvent event) {
    // 放入 MessageQueue,唤醒 App 主线程
    onInputEvent(event, seq);
}
  • 事件处理分发 deliverInputEvent 被调用的地方。
java 复制代码
// frameworks/base/core/java/android/view/ViewRootImpl.java

final class WindowInputEventReceiver extends InputEventReceiver {
    public void onInputEvent(InputEvent event, int seq) {
        enqueueInputEvent(event, this, seq, true); // 放入主线程队列
    }
}

// 最终由主线程 Handler 执行:
void doProcessInputEvents() {
    while (mPendingInputEventHead != null) {
        QueuedInputEvent q = mPendingInputEventHead;
        // 核心调用链在此:
        deliverInputEvent(q);
    }
}

void deliverInputEvent(QueuedInputEvent q) {
    // 这一步将事件交给第一个 Stage (流水线处理)
    q.mStage.deliver(q);
}
  • finishInputEvent回调
java 复制代码
final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
            List<InputEvent> processedEvents;
            try {
                processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvents != null) {
                if (processedEvents.isEmpty()) {
                    // 告诉InputDispatcher完成事件,不然可能导致,Input dispatching timed out (ANR)
                    finishInputEvent(event, true);
                } else {
                    for (int i = 0; i < processedEvents.size(); i++) {
                        // 生成新事件,兼容层把 一个事件变成多个事件。deliverInputEvent()
                        enqueueInputEvent(
                                processedEvents.get(i), this,
                                QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
                    }
                }
            } else {
                // 生成新事件,兼容层把 一个事件变成多个事件。最终调用deliverInputEvent()
                enqueueInputEvent(event, this, 0, true);
            }
        }
}
  • deliverInputEvent()
java 复制代码
private void deliverInputEvent(QueuedInputEvent q) {
        // 省略代码
        try {
            if (mInputEventConsistencyVerifier != null) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "verifyEventConsistency");
                try {
                    mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
            }

            InputStage stage;
            if (q.shouldSendToSynthesizer()) {
                stage = mSyntheticInputStage;
            } else {
                stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
            }

            if (q.mEvent instanceof KeyEvent) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "preDispatchToUnhandledKeyManager");
                try {
                    // key事件预派发处理
                    mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
            }

            if (stage != null) {
                // 窗口焦点处理
                handleWindowFocusChanged();
                stage.deliver(q);
            } else {
                 // 告诉InputDispatcher完成事件,不然可能导致,Input dispatching timed out (ANR)
                finishInputEvent(q);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

3、流程图概况



相关推荐
TT_Close2 小时前
【Flutter×鸿蒙】debug 包也要签名,这点和 Android 差远了
android·flutter·harmonyos
Kapaseker3 小时前
2026年,我们还该不该学编程?
android·kotlin
雨白19 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk19 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING20 小时前
RN容器启动优化实践
android·react native
恋猫de小郭1 天前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker1 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴1 天前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭2 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter