[Framework] Android Handler 工作原理

作者:Tans5

Android 中的 Handler 都被人说烂了,但是还是想多说一次,因为在 Android 的系统中它真的非常重要而且它的机制并没有很复杂,无论是新手和老手都可以好好学习下,这对理解 Android 系统很重要,所以说学习的性价比非常高。

Android 中 IPC 跨进程通信主要依靠 Binder , 而同一个进程的通信就主要依靠 Handler

这里先简单描述下 Handler 的工作流程:

首先在 Handler 处理任务的线程需要调用 Looper#prepare() 方法,来为当前线程创建一个 Looper 实例,这个实例通过 ThreadLocal 存放,也就是一个线程只能有一个这个实例;Looper 构造函数中会初始化一个 MessageQueue 对象,MessageQueue 对象就是用来封装任务的队列,它是用的链表实现,而任务队列的 Item 就是 Message 对象,也就是单个的任务;任务处理线程调用 Looper#prepare() 方法完成后,会调用 Looper#loop() 方法,这个时候任务线程就会阻塞去处理对应 Looper 中的 MessageQueue 中的任务,处理的方式就是一个死循环从 MessageQueue 中去获取最新的 Message 获取到后,然后调用对应的 Handler#handleMessage() 方法去执行它,执行完成后又去获取下一个任务,如果没有新的任务就会陷入阻塞,等有新的任务来的时候会唤醒这个阻塞(这个唤醒机制是用的 Linux 中的 epoll 机制),继续执行新的任务。

上面大致描述了 LooperMessageQueue 如何从队列中获取新的任务和执行任务,这里再简单描述下怎么插入任务,首先要自定一个 Handler 对象,构造函数中需要传入上面所创建的 Looper 对象,其中自定义的 Handler#handleMessage 方法就是用来执行任务的方法,其他的线程需要向对应的 Looper 线程添加任务时,就调用上面 Handler 实例的 sendMessage 方法来添加任务,也就是向 MessageQueue 队列中添加任务,最终会在 Looper 所对应的线程执行,这样就完成了一次跨线程的通信。

下面是一个简单的 Handler 使用的代码:

复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    var handlerLooper: Looper? = null
    val handlerThread = object : Thread() {
        override fun run() {
            super.run()
            Looper.prepare()
            handlerLooper = Looper.myLooper()
            Looper.loop()
        }
    }
    handlerThread.start()
    while (handlerLooper == null) {
        // 等待 HandlerThread 初始化完成
    }
    val handler = object : Handler(handlerLooper!!) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            if (msg.what == 0) {
                // TODO: 处理任务,该方法最终工作在 HandlerThread 线程
            }
            
        }
    }
    
    val msg = handler.obtainMessage(0)
    // 发送的代码工作在主线程
    handler.sendMessage(msg)
}

Android 中有现成的 HandlerThread,不用自定义,我这里是为了展示这个过程。

Tips: 我后续的源码分析都是基于 andorid 31

Looper

Looper 初始化

复制代码
// ...
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
// ...

复制代码
// ...
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
// ...

初始化的过程非常简单,如果当前线程已经有 Looper 对象就直接报错,如果没有新建一个 Looper 对象放在 ThreadLocal 中,在 Looper 的构造函数中,创建了 MessageQueue 实例和保存了当前的 Thread 对象。

Looper 任务处理

复制代码
// ...
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
        Slog.w(TAG, "Loop again would have the queued messages be executed"
                + " before this one completed.");
    }

    me.mInLoop = true;

    // ..

    me.mSlowDeliveryDetected = false;

    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}
// ...

Looper#loop() 方法也非常简单,修改了当前的 Looper 的一些状态,然后在死循环中无限调用 loopOnce() 方法去执行任务,如果该方法返回 false 就表示该 Looper 已经退出,就跳出循环。

复制代码
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }

    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
        logging.println(">>>>> Dispatching to " + msg.target + " "
                + msg.callback + ": " + msg.what);
    }
    
    // ...
    
    Object token = null;
    if (observer != null) {
        token = observer.messageDispatchStarting();
    }
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
    try {
        msg.target.dispatchMessage(msg);
        if (observer != null) {
            observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
        if (observer != null) {
            observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
    } finally {
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
            Trace.traceEnd(traceTag);
        }
    }
    
    
    if (logging != null) {
       logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }
    
    // ...

    msg.recycleUnchecked();

    return true;
}

直接调用 MessageQueue#next() 方法去获取 Message 对象,如果返回空就表示 Looper 已经退出了。这里有一个 Printer 对象,在 Message 执行前后都会打印对应的日志,可以通过调用 Looper#setMessageLogging() 方法来自定义这个对象,根据打印的日志内容我们就可以分辨是开始前还是开始后,在 APM 中经常通过这个方法来判断主线程的任务执行是否超时。执行任务会调用的是 handler.target#dispatchMessage() 方法,其实这个 target 对象就是 Handler,后续再分析这个方法。

这次看代码还有新发现,这里还多了一个 Observer 对象,我记得以前的旧版本是没有的,可以通过 Looper#setObserver() 方法来设置,它的接口如下:

复制代码
public interface Observer {
    /**
     * Called right before a message is dispatched.
     *
     * <p> The token type is not specified to allow the implementation to specify its own type.
     *
     * @return a token used for collecting telemetry when dispatching a single message.
     *         The token token must be passed back exactly once to either
     *         {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}
     *         and must not be reused again.
     *
     */
    Object messageDispatchStarting();

    /**
     * Called when a message was processed by a Handler.
     *
     * @param token Token obtained by previously calling
     *              {@link Observer#messageDispatchStarting} on the same Observer instance.
     * @param msg The message that was dispatched.
     */
    void messageDispatched(Object token, Message msg);

    /**
     * Called when an exception was thrown while processing a message.
     *
     * @param token Token obtained by previously calling
     *              {@link Observer#messageDispatchStarting} on the same Observer instance.
     * @param msg The message that was dispatched and caused an exception.
     * @param exception The exception that was thrown.
     */
    void dispatchingThrewException(Object token, Message msg, Exception exception);
}

Observer 可以监听任务开始,结束和异常,看上去它可以替换 Printer,不过它是对应用层隐藏的,需要骚操作。

Handler

Handler 初始化

复制代码
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

其中 LooperMessageQueue 就不用多说了;其中 mCallback 对象就是可以优先于 Handler#handleMessage 方法处理任务,也可以拦截任务不让 Handler#handleMessage 执行;async 是表示发送的消息是否是异步消息,这个和消息屏障关系密切,后续会再分析。

Handler 发送消息

复制代码
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

无论是调用 Handler 的哪个方法发送消息,最终都会到 enqueueMessage() 方法里面去,在 Looper 中我们说到 target 就是 Handler 对象,在这里就得到验证了,如果 Handler 的构造函数中设置为异步,Message 也会被设置为异步,然后直接将处理好的 Message 通过 MessageQueue#enqueueMessage() 方法添加到队列中。

Handler 处理消息

Looper 下发消息时说到最后会调用 HandlerdispatchMessage() 方法:

复制代码
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

如果 Message 中有设置 Callback 就直接在对应的 Callback 中处理,不继续下发; 如果 Message 中没有设置 Callback,会先检查 Handler 是否有设置 mCallback,如果有设置,调用 mCallback 处理,mCallbackhandleMessage 返回 true 就表示要拦截该消息,如果不拦截就交由 HandlerhandleMessage() 方法处理。

MessageQueue

MessageQueue 插入任务

在讲 Handler 的时候说到插入消息会直接调用 MessageQueue#enqueueMessage() 方法:

复制代码
boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

如果 Message 没有设置 Handler 直接抛出异常;如果 Message 已经是 inUse 状态,直接抛出异常;如果 MessageQueue 已经退出,就直接回收 Message;按照任务的触发时间由小到大,插入到 mMessage 链表中,最后如果需要唤醒 next() 方法中的 nativePollOnce() 方法,就会调用 nativeWake 方法,这是一个 C++ 实现的方法(内部实现其实是向一个管道 fd 中写入了一个数字 1,通过 epoll 机制,会监听到管道 fd 有数据写入,然后会唤醒 nativePollOnce() 方法,然后去读取新的 Message)。

MessageQueue 读取任务

在讲 Looper 时讲到最后获取任务是通过 MessageQueue#next() 方法:

复制代码
Message next() {
    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 等待 epoll 唤醒,唤醒是等待 nativeWake() 方法调用或者超时(超时时间为 nextPollTimeoutMillis)
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // target 为空,就表示这是一个屏障的消息
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                // 找到屏障消息后的第一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    // 当前消息的时间大于当前的时间,表示还没有到达执行的时间,计算间隔,等待重新进入 nativePollOnce 方法
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 获取消息成功,把它从队列中移除,然后返回方法
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }
            
            // 后续就是在没有消息时回调 IdleHandler


            // If first time idle, then get the number of idlers to run.
            // Idle handles only run if the queue is empty or if the first message
            // in the queue (possibly a barrier) is due to be handled in the future.
            // 如果 pendingIdleHandlerCount 小于 0 就表示当前的这一次 next() 方法调用,还没有执行过 IdleHandler,就需要去读取 IdleHandler 的数量,在一次 next() 方法调用时最多执行一次 IdleHandler。
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 这里就表示 IdleHandler 为空,或者已经执行过了。
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                // 执行 idleHandler
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }
            
            // 如果不需要保留就把它移出
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        // 这里设置为 0 后,IdleHandler 就不会再次执行,需要等待下次的 next() 方法调用
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

通过 nativePollOnce() 方法等待唤醒(通过调用 nativeWake() 方法)或者超时(超时时间是 nextPollTimeoutMillis),唤醒后直接获取最新的一条 Message(先跳过同步消息逻辑),然后判断任务执行的时间,如果大于当前时间就继续等待,直到可以执行,如果小于当前时间直接返回,同时把这条消息从队列中移除(先跳过 IdleHandler 逻辑)。

同步消息和消息屏障

如果 target 对象为空就表示该消息是一个同步屏障,如果是同步屏障的话他就会重新再去取该同步屏障消息后面的最新一个异步消息去执行,如果没有异步消息就会去等待。

通过以下方法添加屏障消息:

复制代码
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

其实就是在消息队列中添加了一个 target 为空的消息,同时在 Messagearg1 对象中添加 token 对象,然后这个 token 也会返回给调用方,这个 token 在移除屏障时需要用到。

通过以下方法可以移除这个消息屏障:

复制代码
public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

请求绘制最终会到 ViewRootImpl#scheduleTraversals() 方法中,在这个方法中就会添加一个屏障:

复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

Choregorapher 收到 Vsync 信号后会发送一个 Handler 的异步 Message:

复制代码
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
        VsyncEventData vsyncEventData) {
        // ..
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
       // ..
}

在绘制完成后会调用 ViewRootImpl#unscheduleTraversals() 方法移除这个屏障:

复制代码
    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
IdleHandler

IdleHandler 就是在 next() 方法获取不到消息时会执行一次,next() 方法调用一次最多只能执行一次 IdleHandler,执行 IdleHandler 的时候就表示当前的 Looper 比较空闲,IdleHandler 的数量最多为 4 个 (在 ActivityonDestroy() 回调就是在 IdleHandler 中执行的,可能写这部分代码的人,认为 onDestroy() 相对其他的生命周期没有那么重要,但是由于 IdleHandler 的特性也会引发一些问题,后续有时间会分析这个问题) 。

可以通过以下方法添加 IdleHandler

复制代码
    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }

epoll 在 Handler 中的工作方式

MessageQueue 的构造函数中会通过 Native 方法 nativeInit() 初始化 epoll,初始化流程会先通过 epoll_create() 方法创建一个 EpollFd,然后构建一个管道的 fd 用于 epoll 来监听它的读写状态,这个 fd 被命名为 WakeFd,然后通过 epoll_ctl() 方法将 WakeFd 添加到 EpollFd 中,以供后续通过 EpollFd 能够监听 WakeFd 的状态。

MessageQueuenext() 方法中会通过 Native 方法 nativePollOnce() 方法去监听上面所提到 WakeFd 的状态,Native 层是通过 epoll_wait() 方法去监听状态,这个方法会等到超时或者 WakeFd 有数据写入就会返回。

MessageQueueenqueueMessage() 方法中插入 Message 后,会判断是否需要唤醒 next() 中的 nativePollOnce() 方法,如果需要唤醒就会调用 Native 方法 nativeWake() 方法,Native 层就会向 WakeFd 中写入一个数字 1,然后 nativePollOnce() 间听到这个 WakeFd 写状态后就会返回,也就解除 next() 方法的阻塞去读取新的 Message

最后聊一聊主线程

在 Android Framework 中有两个主要的主线程 Handler,一个是 ActivityThread 中的 H,它主要来处理四大组建的各种生命周期;还有一个是 Choreographer 中的 FrameHandler,它主要负责绘制,动画,输入等操作。这两个 Handler 负责的工作和用户体验都极为密切,主线程也可以说非常忙。

在项目中我经常发现很多的代码都会在主线程执行,明明有的操作不需要主线程执行。比如说在一个网络请求的通用方法中,在网络完成的回调中就会主动切换到主线程,明明很多地方的代码不需要用到主线程,我个人的观点是一定需要切换主线程时再切换,在一定程度上能够缓解应用卡顿的问题。

如果你还没有掌握Framework,现在想要在最短的时间里吃透它,可以参考一下《Android Framework核心知识点》,里面内容包含了:Init、Zygote、SystemServer、Binder、Handler、AMS、PMS、Launcher......等知识点记录。

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:

1.宏观理论分析与Message源码分析

2.MessageQueue的源码分析

3.Looper的源码分析

4.handler的源码分析

5.总结

Binder 原理:

1.学习Binder前必须要了解的知识点

2.ServiceManager中的Binder机制

3.系统服务的注册过程

4.ServiceManager的启动过程

5.系统服务的获取过程

6.Java Binder的初始化

7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程

2.APK的安装和卸载源码分析

3.PMS中intent-filter的匹配架构

WMS:

1.WMS的诞生

2.WMS的重要成员和Window的添加过程

3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战
相关推荐
Yang-Never42 分钟前
Git -> Git使用Patch失败error: patch failed: patch does not apply的解决办法
android·git·android studio
woodWu2 小时前
Android编译时动态插入代码原理与实践
android
百锦再3 小时前
Android Studio 实现自定义全局悬浮按钮
android·java·ide·app·android studio·安卓
百锦再3 小时前
Android Studio 项目文件夹结构详解
android·java·ide·ios·app·android studio·idea
mokkaio3 小时前
如何设计代码逻辑
架构
omnibots3 小时前
ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(输入类外设之触摸屏 Touch)
嵌入式硬件·架构·iot
老码识土3 小时前
Kotlin 协程源代码泛读:Continuation
android·kotlin
行墨5 小时前
Replugin 的hook点以及hook流程
android·架构
一一Null5 小时前
Access Token 和 Refresh Token 的双令牌机制,维持登陆状态
android·python·安全·flask
桂月二二5 小时前
Vue3服务端渲染(SSR)深度调优:架构裂变与性能突围
架构