【Android】浅析View.post()

【Android】浅析View.post()原理

本文参考:

Android 之你真的了解 View.post() 原理吗? - 简书

我们知道在onResume()方法中是无法准确获取到View的宽高的,这主要是因为View绘制流程开始的实际在onResume()方法之后。无法确认在onResume()方法中View的宽高为最终值。

一般情况下,我们使用View.post()解决问题。

View.post()为什么能获取到view的实际宽高?

View.post()

View 的 post 方法如下:

Java 复制代码
public boolean post(Runnable action) {
    // 首先判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果不为null,直接调用其内部Handler的post
        return attachInfo.mHandler.post(action);
    }

    // 否则加入当前View的等待队列
    getRunQueue().post(action);
    return true;
}

若 View 已附着到窗口(attachInfo != null),直接通过窗口线程的 Handler 执行任务,确保任务在 UI 线程执行。

AttachInfo 存在:直接通过窗口线程的 Handler 提交任务(UI 线程)。

AttachInfo 不存在 :将任务存入本地队列 HandlerActionQueue,等待 View 附着后执行。

注意 AttachInfo 是 View 的静态内部类,每个 View 都会持有一个 AttachInfo,它默认为 null。

AttachInfo的几个关键属性:

WindowManager.LayoutParams:包含 View 的布局参数(如宽高、位置、对齐方式等)。

ViewRootImpl :指向当前 View 树的根节点对应的 ViewRootImpl 对象(负责协调 View 树的测量、布局和绘制)。

Context:提供应用上下文,用于获取资源(如字符串、Drawable 等)。

Display:当前窗口对应的屏幕显示信息(如尺寸、密度等)。

先来看下 getRunQueue ().post ():

java 复制代码
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

getRunQueue () 返回的是 HandlerActionQueue,也就是调用了 HandlerActionQueue 的 post 方法:

java 复制代码
public void post(Runnable action) {
    // 调用到postDelayed方法,这有点类似于Handler发送消息
    postDelayed(action, 0);
}

// 实际调用postDelayed
public void postDelayed(Runnable action, long delayMillis) {
    // HandlerAction表示要执行的任务
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            // 创建一个保存HandlerAction的数组
            mActions = new HandlerAction[4];
        }
        // 表示要执行的任务HandlerAction 保存在 mActions 数组中
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        // mActions数组下标位置累加1
        mCount++;
    }
}

HandlerAction 表示一个待执行的任务,内部持有要执行的 Runnable 和延迟时间;类声明如下:

java 复制代码
private static class HandlerAction {
    // post的任务
    final Runnable action;
    // 延迟时间
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }

    // 比较是否是同一个任务
    // 用于匹配某个 Runnable 和对应的HandlerAction
    public boolean matches(Runnable otherAction) {
        return otherAction == null && action == null
                || action != null && action.equals(otherAction);
    }
}

注意 postDelayed () 创建一个默认长度为 4 的 HandlerAction 数组,用于保存 post () 添加的任务;跟踪到这,大家是否有这样的疑惑:View.post () 添加的任务没有被执行?

实际上,此时我们要回过头来,重新看下 AttachInfo 的创建过程,先看下它的构造方法:

java 复制代码
AttachInfo(IWindowSession session, IWindow window, Display display,
               ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
               Context context) {
        mSession = session;
        mWindow = window;
        mWindowToken = window.asBinder();
        mDisplay = display;
        // 持有当前ViewRootImpl
        mViewRootImpl = viewRootImpl;
        // 当前渲染线程Handler
        mHandler = handler;
        mRootCallbacks = effectPlayer;
        // 为其创建一个ViewTreeObserver
        mTreeObserver = new ViewTreeObserver(context);
    }

注意 AttachInfo 中持有当前线程的 Handler。翻阅 View 源码,发现仅有两处对 mAttachInfo 赋值操作,一处是为其赋值,另一处是将其置为 null。

mAttachInfo 赋值过程:

java 复制代码
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 给当前View赋值AttachInfo,此时所有的View共用同一个AttachInfo(同一个ViewRootImpl内)
    mAttachInfo = info;
    // View浮层,是在Android 4.3添加的
    if (mOverlay != null) {
        // 任何一个View都有一个ViewOverlay
        // ViewGroup的是ViewGroupOverlay
        // 它区别于直接在类似RelativeLaout/FrameLayout添加View,通过ViewOverlay添加的元素没有任何事件
        // 此时主要分发给这些View浮层
        mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
    }
    mWindowAttachCount++;

     // ... 省略

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {
        mAttachInfo.mScrollContainers.add(this);
        mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
    }
    //  mRunQueue,就是在前面的 getRunQueue().post()
    // 实际类型是 HandlerActionQueue,内部保存了当前View.post的任务
    if (mRunQueue != null) {
        // 执行使用View.post的任务
        // 注意这里是post到渲染线程的Handler中
        mRunQueue.executeActions(info.mHandler);
        // 保存延迟任务的队列被置为null,因为此时所有的View共用AttachInfo
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    // 回调View的onAttachedToWindow方法
    // 在Activity的onResume方法中调用,但是在View绘制流程之前
    onAttachedToWindow();

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
        // 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
        listener.onViewAttachedToWindow(this);
    }

    // ...  省略

    // 回调View的onVisibilityChanged
    // 注意这时候View绘制流程还未真正开始
    onVisibilityChanged(this, visibility);

    // ... 省略
}

方法最开始为当前 View 赋值 AttachInfo。注意 mRunQueue 就是保存了 View.post () 任务的 HandlerActionQueue;此时调用它的 executeActions 方法如下:

java 复制代码
public void executeActions(Handler handler) {
    synchronized (this) {
        // 任务队列
        final HandlerAction[] actions = mActions;
        // 遍历所有任务
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            //发送到Handler中,等待执行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        //此时不在需要,后续的post,将被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}

遍历所有已保存的任务,发送到 Handler 中排队执行;将保存任务的 mActions 置为 null,因为后续 View.post () 直接添加到 AttachInfo 内部的 Handler 。所以不得不去跟踪 dispatchAttachedToWindow () 的调用时机。

ViewRootImpl

每个 Activity 对应一个 Window,而 Window 的根视图是 DecorView。因此,一个 Activity 内的所有 View 共享同一个 AttachInfo

在 View 绘制流程启动时,ViewRootImpl 通过 host.dispatchAttachedToWindow(mAttachInfo, 0) 将 AttachInfo 传递给 DecorView:

java 复制代码
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);

一般 Activity 包含多个 View 形成 View Hierachy 的树形结构,只有最顶层的 DecorView 才是对 WindowManagerService "可见的"。

dispatchAttachedToWindow () 的调用时机是在 View 绘制流程的开始阶段。在 ViewRootImpl 的 performTraversals 方法,在该方法将会依次完成 View 绘制流程的三大阶段:测量、布局和绘制,不过这部分不是今天要分析的重点。

java 复制代码
// View 绘制流程开始在 ViewRootImpl
private void performTraversals() {
    // mView是DecorView
    final View host = mView;
    if (mFirst) {
        .....
        // host为DecorView
        // 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
        .....
    } 
   mFirst=false
   ...
   // Execute enqueued actions on every traversal in case a detached view   enqueued an action
   getRunQueue().executeActions(mAttachInfo.mHandler);
   // View 绘制流程的测量阶段
   performMeasure();
   // View 绘制流程的布局阶段
   performLayout();
   // View 绘制流程的绘制阶段
   performDraw();
   ...

}

host 的实际类型是 DecorView,DecorView 继承自 FrameLayout。

每个 Activity 都有一个关联的 Window 对象,用来描述应用程序窗口,每个窗口内部又包含一个 DecorView 对象,DecorView 对象用来描述窗口的视图 --- xml 布局。通过 setContentView () 设置的 View 布局最终添加到 DecorView 的 content 容器中。

跟踪 DecorView 的 dispatchAttachedToWindow 方法的执行过程,DecorView 并没有重写该方法,而是在其父类 ViewGroup 中:

java 复制代码
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    // 子View的数量
    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有子View
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // 遍历调用所有子View的dispatchAttachedToWindow
        // 为每个子View关联AttachInfo
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    // ...
}

for 循环遍历当前 ViewGroup 的所有 childView,为其关联 AttachInfo。子 View 的 dispatchAttachedToWindow 方法在前面我们已经分析过了:首先为当前 View 关联 AttachInfo,然后将之前 View.post () 保存的任务添加到 AttachInfo 内部的 Handler 。

当 View 首次被添加到窗口时,dispatchAttachedToWindow() 会在 performTraversals() 中被调用,此时会发生两件事:

  1. AttachInfo 被传递给所有子 View
  2. 本地队列中的任务被迁移到窗口 Handler

关键代码如下:

java 复制代码
// ViewRootImpl.performTraversals()
private void performTraversals() {
    if (mFirst) {
        host.dispatchAttachedToWindow(mAttachInfo, 0); // 传递 AttachInfo 并执行本地队列
    }
    
    // 执行完 dispatchAttachedToWindow 后,再进行测量、布局、绘制
    performMeasure();
    performLayout();
    performDraw();
    
    // 最后执行 ViewRootImpl 自身队列中的任务(与 View.post() 无关)
    getRunQueue().executeActions(mAttachInfo.mHandler);
}

这里的关键在于:dispatchAttachedToWindow() 在测量、布局、绘制之前执行,但任务迁移到 Handler 后,需要等待当前消息处理完成才能执行。

因此,View.post () 的任务实际会在当前绘制周期结束后执行,此时 View 已经完成了布局和绘制,可以安全获取宽高。

那么我们就可以回答开头的问题:

为什么 View.post () 能可靠获取 View 尺寸?

  1. View 首次布局前 :调用 view.post(),任务存入本地队列
  2. 绘制流程启动dispatchAttachedToWindow() 被调用,AttachInfo 传递给所有 View
  3. 本地队列任务迁移:任务被发送到窗口 Handler 的消息队列尾部
  4. 当前绘制周期完成:测量、布局、绘制全部结束
  5. 执行 View.post () 的任务:此时 View 尺寸已经确定

碎片化问题

当你创建一个独立的 View 并调用 post () 时:

java 复制代码
final ImageView view = new ImageView(this);
    view.post(new Runnable() {
        @Override
        public void run() {
            // do something
        }
    });

此时 View 的 mAttachInfo 为 null,任务会被存入 HandlerActionQueue。但由于该 View 未被添加到窗口,dispatchAttachedToWindow() 永远不会被调用,导致:

  1. AttachInfo 未被传递给该 View
  2. 本地队列中的任务永远不会被迁移到 UI 线程执行

这就是独立 View 的 post () 任务无法执行的根本原因。

不过可以将View添加到窗口,从而主动触发View绘制流程。当你调用 contentView.addView(view) 时,系统会:

  1. 将新 View 添加到 ViewGroup 中
  2. 标记该 ViewGroup 需要重新布局
  3. 在下一帧绘制时触发整个 View 树的重绘

例如:

java 复制代码
// 将View添加到窗口
// 此时重新发起绘制流程,post任务会被执行
contentView.addView(view);

关键代码路径如下:

java 复制代码
// ViewGroup.addView()
public void addView(View child, int index) {
    // ...
    requestLayout(); // 标记需要重新布局
    invalidate(true); // 标记需要重绘
}

// ViewRootImpl.performTraversals()
private void performTraversals() {
    if (mFirst || ...) { // 首次绘制或需要重新布局
        host.dispatchAttachedToWindow(mAttachInfo, 0); // 传递 AttachInfo
    }
    // 执行测量、布局、绘制
}

AttachInfo是什么

AttachInfo是一个包含了大量关于View如何与窗口关联以及如何绘制自身的数据结构。当一个View被附加(attached)到Window上时,系统会为这个View创建并填充一个AttachInfo对象。同一个window下的所有View,持有的AttachInfo都是同一份。

创建View#AttachInfoViewRootImpl生成,mAttachInfo实例维护在ViewRootImpl中。在ViewRootImpl的构造函数中会创建AttachInfo对象,示例代码如下:

java 复制代码
ViewRootImpl(Context context, Display display, IWindowSession session, boolean useSfChoreographer) {
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
}
  • Activity启动时渲染布局 :调用流程为ViewRootImpl#doTraversal() -> ViewRootImpl#performTraversals() -> Decorview#dispatchAttachedToWindow(AttachInfo info, int visibility) -> ViewGroup#dispatchAttachedToWindow(AttachInfo info, int visibility) -> View#dispatchAttachedToWindow(AttachInfo info, int visibility),将AttachInfo关联到对应的View。
  • 动态添加View :例如点击button时给root添加子View,调用栈为View.OnClickListener#onClick(View v) -> ViewGroup#addView(View child, LayoutParams params) -> ViewGroup#addView(View child, int index, LayoutParams params) -> ViewGroup#addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) -> View#dispatchAttachedToWindow(AttachInfo info, int visibility)。最终保证了整个View树中的View#AttachInfo是同一个对象。

mAttachInfo 置 null 的过程

当 View 从窗口中移除时,需要释放 AttachInfo 以避免内存泄漏。这一过程由 dispatchDetachedFromWindow() 方法触发,实际是调用其父类 ViewGroup :

java 复制代码
// ViewGroup.dispatchDetachedFromWindow()
void dispatchDetachedFromWindow() {
    // 递归释放所有子 View 的 AttachInfo
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        children[i].dispatchDetachedFromWindow(); // 关键递归点
    }
    
    // 父类 View 释放逻辑
    super.dispatchDetachedFromWindow();
}

// View.dispatchDetachedFromWindow()
void dispatchDetachedFromWindow() {
    // 回调用户逻辑
    onDetachedFromWindow();
    
    // 通知监听器
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnAttachStateChangeListeners != null) {
        for (OnAttachStateChangeListener listener : li.mOnAttachStateChangeListeners) {
            listener.onViewDetachedFromWindow(this); // 触发监听器回调
        }
    }
    
    // 释放核心引用
    mAttachInfo = null; // 置空 AttachInfo
    mOverlay = null;    // 释放浮层引用
}

递归释放 :ViewGroup 会先释放所有子 View 的 AttachInfo,确保资源释放顺序为 子→父

回调顺序 :先执行 onDetachedFromWindow()(用户自定义逻辑),再通知监听器,最后置空引用。

java 复制代码
void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
    if (info != null) {
        int vis = info.mWindowVisibility;
        if (vis != GONE) {
            // 通知 Window显示状态发生变化
            onWindowVisibilityChanged(GONE);
            if (isShown()) {
                onVisibilityAggregated(false);
            }
        }
    }
    // 回调View的onDetachedFromWindow
    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    // ... 省略

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知回调 onViewDetachedFromWindow
            listener.onViewDetachedFromWindow(this);
        }
    }

    // ... 省略

    // 将AttachInfo置为null
    mAttachInfo = null;
    if (mOverlay != null) {
        // 通知浮层View
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }

    notifyEnterOrExitForAutoFillIfNeeded(false);
}

可以看到在 dispatchDetachedFromWindow 方法,首先回调 View 的 onDetachedFromWindow (),然后通知所有监听者 onViewDetachedFromWindow (),最后将 mAttachInfo 置为 null。

由于 dispatchAttachedToWindow 方法是在 ViewRootImpl 中完成,此时很容易想到它的释放过程肯定也在 ViewRootImpl,跟踪发现如下调用过程:

java 复制代码
void doDie() {
    // 检查执行线程
    checkThread();

    synchronized (this) {
        if (mRemoved) {
            return;
        }
        mRemoved = true;
        if (mAdded) {
            // 回调View的dispatchDetachedFromWindow
            dispatchDetachedFromWindow();
        }

        if (mAdded && !mFirst) {
            destroyHardwareRenderer();

            // mView是DecorView
            if (mView != null) {
                int viewVisibility = mView.getVisibility();
                // 窗口状态是否发生变化
                boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                if (mWindowAttributesChanged || viewVisibilityChanged) {
                    try {
                        if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                            mWindowSession.finishDrawing(mWindow);
                        }
                    } catch (RemoteException e) {
                    }
                }
                // 释放画布
                mSurface.release();
            }
        }

        mAdded = false;
    }

    // 将其从WindowManagerGlobal中移除
    // 移除DecorView
    // 移除DecorView对应的ViewRootImpl
    // 移除DecorView
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

可以看到 dispatchDetachedFromWindow 方法被调用,注意方法最后将 ViewRootImpl 从 WindowManager 中移除。

经过前面的分析我们已经知道 AttachInfo 的赋值操作是在 View 绘制任务的开始阶段,而它的调用者是 ActivityThread 的 handleResumeActivity 方法,即 Activity 生命周期 onResume 方法之后。

那它是在 Activity 的哪个生命周期阶段被释放的呢?在 Android 中, Window 是 View 的容器,而 WindowManager 则负责管理这些窗口,具体可以参考《View 绘制流程之 DecorView 添加至窗口的过程 》。

我们直接找到管理应用进程窗口的 WindowManagerGlobal,查看 DecorView 的移除工作:

java 复制代码
/**
 * 将DecorView从WindowManager中移除
 */
public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    synchronized (mLock) {
        // 找到保存该DecorView的下标,true表示找不到要抛出异常
        int index = findViewLocked(view, true);
        // 找到对应的ViewRootImpl,内部的DecorView
        View curView = mRoots.get(index).getView();
        // 从WindowManager中移除该DecorView
        // immediate 表示是否立即移除
        removeViewLocked(index, immediate);
        if (curView == view) {
            // 判断要移除的与WindowManager中保存的是否为同一个
            return;
        }

        // 如果不是同一个View(DecorView),抛异常
        throw new IllegalStateException("Calling with view " + view
                + " but the ViewAncestor is attached to " + curView);
    }
}

根据要移除的 DecorView 找到在 WindowManager 中保存的 ViewRootImpl,真正移除是在 removeViewLocked 方法:

java 复制代码
private void removeViewLocked(int index, boolean immediate) {
    // 找到对应的ViewRootImpl
    ViewRootImpl root = mRoots.get(index);
    // 该View是DecorView
    View view = root.getView();

    // ... 省略
    
    // 调用ViewRootImpl的die
    // 并且将当前ViewRootImpl在WindowManagerGlobal中移除
    boolean deferred = root.die(immediate);
    if (view != null) {
        // 断开DecorView与ViewRootImpl的关联
        view.assignParent(null);
        if (deferred) {
            // 返回 true 表示延迟移除,加入待死亡队列
            mDyingViews.add(view);
        }
    }
}

可以看到调用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:

java 复制代码
boolean die(boolean immediate) {
    // immediate 表示立即执行
    // mIsInTraversal 表示是否正在执行绘制任务
    if (immediate && !mIsInTraversal) {
        // 内部调用了View的dispatchDetachedFromWindow
        doDie();
        // return false 表示已经执行完成
        return false;
    }

    if (!mIsDrawing) {
        // 释放硬件加速绘制
        destroyHardwareRenderer();
    } 
    // 如果正在执行遍历绘制任务,此时需要等待遍历任务完成
    // 故发送消息到尾部
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

注意 doDie 方法(源码在前面已经贴出),它最终会调用 dispatchDetachedFromWindow 方法。

最后,移除 Window 窗口任务是通过 ActivityThread 完成的,具体调用在 handleDestoryActivity 方法完成:

java 复制代码
private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    // 回调 Activity 的 onDestory 方法
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);

        // 获取当前Window的WindowManager, 实际是WindowManagerImpl
        WindowManager wm = r.activity.getWindowManager();
        // 当前Window的DecorView
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            // Window 是否添加过,到WindowManager
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    r.window.clearContentView();
                } else {
                    // 通知 WindowManager,移除当前 Window窗口
                    wm.removeViewImmediate(v);
                }
            }
} 

注意 performDestoryActivity () 将完成 Activity 生命周期 onDestory 方法回调。然后调用 WindowManager 的 removeViewImmediate ():

java 复制代码
/**
 * WindowManagerImpl
 */
@Override
public void removeViewImmediate(View view) {
    // 调用WindowManagerGlobal的removeView方法
    mGlobal.removeView(view, true);
}

即 AttachInfo 的释放操作是在 Activity 生命周期 onDestory 方法之后,在整个 Activity 的生命周期内都可以正常使用 View.post () 任务。

相关推荐
沐怡旸4 小时前
深入解析 Android Performance Analyzer (APA) 底层架构与技术原理
android
李斯维11 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
plainGeekDev14 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户416596736935514 小时前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev14 小时前
onActivityResult → ActivityResult API
android·java·kotlin
随遇丿而安18 小时前
第10周:Activity 基础功能与生命周期优化
android
alexhilton1 天前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
落魄Android在线炒饭2 天前
Android 自定义HAL开发篇之 HIDL篇——从入门到实战(上)
android
plainGeekDev2 天前
广播接收器 → Flow + Lifecycle
android·java·kotlin
plainGeekDev2 天前
EventBus → SharedFlow
android·java·kotlin