Android Framework-WMS-从setContentView开始

今天是嗯啊战神面试的又一天,面试官说你写熟悉WMS,能简单讲一下WMS中窗口的构建过程么? 我说纳尼?what?吗?~~

涉及类

  • ViewRootImpl
  • WindowManagerGlobal
  • WindowManagerImpl
  • ActivityThread
  • PhoneWindow
  • DecorView
  • WindowState
  • WindowToken

前菜系列之PhoneWindow 从哪里来的

ActivityThreadperformLaunchActivity 方法中,调用了反射构建了Activity, 然后 调用了attach 方法,在该方法里直接 构建了出了PhoneWindow

java 复制代码
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
        IBinder shareableActivityToken) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    }

大家肯定都习惯在onCreate里面,熟练的写下 setContentView(xxx);

java 复制代码
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();  
}

public Window getWindow() {
    return mWindow;
}

这里其实就是调用了PhoneWindow``的setContentView 方法

java 复制代码
//frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } 

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
   
}

这里我们要介绍一下mContentParent installDecor我就想知道他俩怎么来的

java 复制代码
//frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
       //省略
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
    //注意 mDecor进去了
        mContentParent = generateLayout(mDecor);
 //省略

}

generateDecor() 方法 其实直接就 new 了一个出来。不贴代码了。

java 复制代码
//frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    TypedArray a = getWindowStyle();

    int layoutResource;
    int features = getLocalFeatures();
     //省略。。。。
    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
    //省略。。。。
        // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        // Special case for a window with only a progress bar (and title).
        // XXX Need to have a no-title version of embedded windows.
        layoutResource = R.layout.screen_progress;
        // System.out.println("Progress!");
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
    
      //省略。。。。
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogCustomTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
       
    } 
    //省略。。。。
       
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
   //省略。。。。

 //省略。。。。

    mDecor.finishChanging();

    return contentParent;
}


//core/java/com/android/internal/policy/DecorView.java
    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
   //省略
        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
     //省略
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

这段代码总结一下,根据特性,找出来一个合适的布局,然后调用mDecor.onResourcesLoaded() ,在该方法里调用 inflater 构建出一个View,然后赋值给了给 mContentRoot。

contentParentDecorView 查找对应布局里的R.id.content DecorView 本身继承了 FrameLayout,也就是相当于DecorView自己有了自己的填充物。 这里摘抄了两个布局,里面都包含了content 这个id。

xml 复制代码
 <!-- frameworks/base/core/res/res/layout/screen_custom_title.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />

    <FrameLayout android:id="@android:id/title_container" 
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        android:transitionName="android:title"
        style="?android:attr/windowTitleBackgroundStyle">
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
xml 复制代码
<!--frameworks/base/core/res/res/layout/screen_simple_overlay_action_mode.xml-->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
</FrameLayout>

继续看一下

java 复制代码
//frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } 

   //省略...
        mLayoutInflater.inflate(layoutResID, mContentParent);
    
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
   
}

我们传的布局 ,最后塞给了DecorView里面的content 这个布局里面。

到现在为止,已经知道我们正常传入一个布局,会被像洋葱一样给包到芯里。

我当时讲到这里,甚至为自己的超级大内存脑袋瓜子沾沾自喜了一下, 面试官问然后呢?

然后,在ActivityThreadhandleResumeActivity 方法中,我们会调用到onresume方法。

java 复制代码
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {

//省略。。
    final Activity a = r.activity;

//省略。。
    if (r.window == null && !a.mFinished && willBeVisible) {
      //PhoneWindwo
        r.window = r.activity.getWindow();
      //decorView
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
      //拿到WindowManager
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        if (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
              
                a.mWindowAdded = true;
              //添加docorView 和LayoutParams
                wm.addView(decor, l);
            }
          //省略。。
        }

        
    } 
//省略...

}

这里代码就很直白。 调用ViewManager.addView()decor 添加了进去。

ViewManager 的实现类是WindowManagerImpl

进入 WindowManagerImpladdView()

java 复制代码
//android/view/WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyTokens(params);
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

WindowManagerGlobal ,对于这个类 ,所有的Window 大小参数 等包含和wms的交互的都归他管理,我们这里简单理解他大管家就行。区区配角而已。

java 复制代码
//frameworks/base/core/java/android/view/WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {

   //省略...
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
      
    //省略...

        IWindowSession windowlessSession = null;
       
         //省略...

        if (windowlessSession == null) {
            root = new ViewRootImpl(view.getContext(), display);
        } else {
            root = new ViewRootImpl(view.getContext(), display,
                    windowlessSession, new WindowlessWindowLayout());
        }

     

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
            // BadTokenException or InvalidDisplayException, clean up.
            if (viewIndex >= 0) {
                removeViewLocked(viewIndex, true);
            }
            throw e;
        }
    }
}

这里大体可以分为3部分

  • ViewRootImpl的构建
  • mViews/mRoots/mParams 添加管理,真大管家模式。
  • ViewRootImpl将我们传过来的view 设置了进去

看一下 ViewRootImpl 的构造函数

java 复制代码
public final Surface mSurface = new Surface();
private final SurfaceControl mSurfaceControl = new SurfaceControl();

public ViewRootImpl(Context context, Display display) {
    this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout());
}

  public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
            WindowLayout windowLayout) {
  
       mWindowSession = session;
      mWindow = new W(this);
      mChoreographer = Choreographer.getInstance();
  }

  static class W extends IWindow.Stub {
    
  }

会调用到四个参数的构造,里面构建了一个静态W类,该类继承IWindow.Stub。我们暂且不展开。注意哦,ViewRootImpl自带SurfaceControlSurface。 这里注意 mWindow = new W(this); 后面要用。

WindowManagerGlobal.getWindowSession() ,也就是说没有Session 不用担心WindowManagerGlobal 调用到WMSopenSession() 构建一个Session

java 复制代码
private static IWindowSession sWindowSession;
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
               InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
              //获取到wms
                IWindowManager windowManager = getWindowManagerService();
              //调用wms的openSession 其实返回是Session类
              //frameworks/base/services/core/java/com/android/server/wm/Session.java
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        });
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return sWindowSession;
    }
}

等等,这玩意好像是个静态的,岂不是所有人用一个?是的,一个进程里面,所有的window 都用一个sWindowSession,是进程级别的。

现在注意我们的DecorView 已经传递给了ViewRootImpl 。 我们继续看setView后做了什么?

java 复制代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
        int userId) {
mView = view;  
  //省略..
           requestLayout();
         InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    inputChannel = new InputChannel();
                }
  
    res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
                            mTempControls, attachedFrame, compatScale);
        
    if (mInputQueueCallback != null) {
                        mInputQueue = new InputQueue();
                        mInputQueueCallback.onInputQueueCreated(mInputQueue);
                    }
                    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
        }
  • 我们构建的
  • requestLayout() 会 触发布局请求 也就是我们那说的绘制流程 。
  • 触摸 按键等事件的接受处理 InputChannelWindowInputEventReceiver
  • 调用mWindowSession 调用 addToDisplayAsUser()mWindow 等参数等添加进去
java 复制代码
//services/core/java/com/android/server/wm/Session.java
    @Override
    public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
            float[] outSizeCompatScale) {
        return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls,
                outAttachedFrame, outSizeCompatScale);
    }

继续调用到了WMSaddWindow()

java 复制代码
//frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
        int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
        InputChannel outInputChannel, InsetsState outInsetsState,
        InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
        float[] outSizeCompatScale) {
//省略...
    WindowState parentWindow = null;
    final int callingUid = Binder.getCallingUid();
    final int callingPid = Binder.getCallingPid();
  //这里之前也被问到过,基本定义服务基本都用到到
    final long origId = Binder.clearCallingIdentity();
    final int type = attrs.type;

    synchronized (mGlobalLock) {
       //省略...
      //找DisplayContent
        final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);

        //省略...  
   
         ActivityRecord activity = null;
      //判断是否有父亲 有就用父亲的windowtoken 没有就得构建了 一般用于subwindow 比如toast  popwindows 、dialog这种
            final boolean hasParent = parentWindow != null;
            WindowToken token = displayContent.getWindowToken(
                    hasParent ? parentWindow.mAttrs.token : attrs.token);

       //省略...  
      
           if (hasParent) {
                  
                    token = parentWindow.mToken;
                } else if (mWindowContextListenerController.hasListener(windowContextToken)) {
                 
             //有则更换
                    final IBinder binder = attrs.token != null ? attrs.token : windowContextToken;
                    final Bundle options = mWindowContextListenerController
                            .getOptions(windowContextToken);
                    token = new WindowToken.Builder(this, binder, type)
                            .setDisplayContent(displayContent)
                           .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
                            .setRoundedCornerOverlay(isRoundedCornerOverlay)
                            .setFromClientToken(true)
                            .setOptions(options)
                            .build();
                } else {
             //我打断点看走了 client.asBinder 这里的client 是IWindow 也就是viewRootImp里的W类
                    final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
                    token = new WindowToken.Builder(this, binder, type)
                            .setDisplayContent(displayContent)
                            .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
                            .setRoundedCornerOverlay(isRoundedCornerOverlay)
                            .build();
                }
            
      
      //windowState的构建
       final WindowState win = new WindowState(this, session, client, token, parentWindow,
                    appOp[0], attrs, viewVisibility, session.mUid, userId,
                    session.mCanAddInternalSystemWindow);
      
      
          win.attach();
            mWindowMap.put(client.asBinder(), win);
            win.initAppOpsState();
     
        win.mToken.addWindow(win);

    return res;
}
  • 构建WindowToken
  • 构建WindowState
  • WindowState 调用 attach()
  • WindowState.mToken.addWindow() 把自己传进去。

先看下 WindowToken

java 复制代码
//frameworks/base/services/core/java/com/android/server/wm/WindowToken.java
WindowToken build() {
            return new WindowToken(mService, mToken, mType, mPersistOnEmpty, mDisplayContent,
                    mOwnerCanManageAppTokens, mRoundedCornerOverlay, mFromClientToken, mOptions);
        }



 protected WindowToken(WindowManagerService service, IBinder _token, int type,
            boolean persistOnEmpty, DisplayContent dc, boolean ownerCanManageAppTokens,
            boolean roundedCornerOverlay, boolean fromClientToken, @Nullable Bundle options) {
        super(service);
        token = _token;
        windowType = type;
        mOptions = options;
        mPersistOnEmpty = persistOnEmpty;
        mOwnerCanManageAppTokens = ownerCanManageAppTokens;
        mRoundedCornerOverlay = roundedCornerOverlay;
        mFromClientToken = fromClientToken;
        if (dc != null) {
            dc.addWindowToken(token, this);
        }
    }

//DisplayContent
  void addWindowToken(IBinder binder, WindowToken token) {
    
       //省略

        mTokenMap.put(binder, token);

        if (token.asActivityRecord() == null) {

            token.mDisplayContent = this;

            final DisplayArea.Tokens da = findAreaForToken(token).asTokens();
            da.addChild(token);
        }
    }

WindowToken 也就是我们一路传递过来的W类。(我们先绕过利用父Token的情况)。 然后 DisplayContentW作为key ,当前widowToken 做为值 存入了自己的mTokenMap。 然后找到对应的DisplayAreatokens 加了进去。

WindowToken , WindowStatetoken的关系,怎么理解好呢? 举个简单例子。

我们爱好学习,构建了青龙 学习小分队,小分队编号为 001。 青龙 学习小分队 就是WindowTokentoken就是小队编号。 WindowState 是啥呢? 是小分队的团 队成员,高矮胖瘦,各不相同。 世界上可能有很多个青龙 学习小分队,但是编号是唯一的。 有一天总部需要我们青龙学习小分队的时候,就靠编号找到我们。

接下来看windowState的相关操作

java 复制代码
     win.attach();
            mWindowMap.put(client.asBinder(), win);
            win.initAppOpsState();
    
        win.mToken.addWindow(win);

attach()

java 复制代码
 void attach() {
     
        mSession.windowAddedLocked();
    }

 void windowAddedLocked() {
        if (mPackageName == null) {
            final WindowProcessController wpc = mService.mAtmService.mProcessMap.getProcess(mPid);
            if (wpc != null) {
                mPackageName = wpc.mInfo.packageName;
                mRelayoutTag = "relayoutWindow: " + mPackageName;
            } 
        }
        if (mSurfaceSession == null) {
           
            mSurfaceSession = new SurfaceSession();
       
            mService.mSessions.add(this);
            if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
                mService.dispatchNewAnimatorScaleLocked(this);
            }
        }
        mNumWindow++;
    }

emmm, 个人理解设置了包名,构建了SurfaceSession() 加入 WMSmSessions 把当前Session 加进去的, 构建 SurfaceSession

win.mToken.addWindow(win) 其实就是之前构建的的windowToken, 把自己传入了进去。

java 复制代码
void addWindow(final WindowState win) {
   
    if (win.isChildWindow()) {
     
        return;
    }

    if (mSurfaceControl == null) {
        createSurfaceControl(true /* force */);
        reassignLayer(getSyncTransaction());
    }
    if (!mChildren.contains(win)) {
      
        addChild(win, mWindowComparator);
        mWmService.mWindowsChanged = true;
       
    }
}


    private final Comparator<WindowState> mWindowComparator =
            (WindowState newWindow, WindowState existingWindow) -> {
        final WindowToken token = WindowToken.this;
       //省略

        return isFirstChildWindowGreaterThanSecond(newWindow, existingWindow) ? 1 : -1;
    };
  • createSurfaceControl(true /* force */); 不拓展开,这里其实就是构建了SurfaceControl 有了SurfaceControl 才能真正进行绘制。青龙学习小分队才有了自己的地盘。

  • addChild(win, mWindowComparator); mWindowComparator 其实就是比较Z轴的高低,进行排序。 简单理解 青龙山学习小组 按个头高矮来排,谁高谁占前面。

    在子类 ActivityReocord 里面 涉及了window的替换逻辑。

java 复制代码
@Override
    void addWindow(WindowState w) {
        super.addWindow(w);

        boolean gotReplacementWindow = false;
        for (int i = mChildren.size() - 1; i >= 0; i--) {
            final WindowState candidate = mChildren.get(i);
            gotReplacementWindow |= candidate.setReplacementWindowIfNeeded(w);
        }

        // if we got a replacement window, reset the timeout to give drawing more time
        if (gotReplacementWindow) {
            mWmService.scheduleWindowReplacementTimeouts(this);
        }
        checkKeyguardFlagsChanged();
    }

其实从ViewRootImplmWindowSession.addToDisplayAsUser()开始,我们可以注意到,其实这里传递的参数 并不是像我最初理解的一样,直接把视图传过去。而是给总部传递了W类做为最核心的钥匙, LayoutParams 等诸多参数。 套用青龙山 学习小组,就是通过中间人(Session)联系联系到指挥WMS挂靠到总部(DisplayContent),我们的编号是多少,我们多少人,怎么联系我们,而不是真的把自己的队伍带去到总部。 总部在收到我们电话后,确认我们的信息真实有效,就把我们挂到了xx分区(DisplayArea)下面。 在此之前,ViewRootImpl 直接吞并(setView()DecorView, 把他变为自己的 mView,成功构建青龙山 学习小组。 可惜DecorView 辛苦忙活半天,居然被人偷家了

简历已经改为 熟悉WMS的拼写

相关推荐
前行的小黑炭4 小时前
Android :Compose如何监听生命周期?NavHostController和我们传统的Activity的任务栈有什么不同?
android·kotlin·app
Lei活在当下12 小时前
【业务场景架构实战】5. 使用 Flow 模式传递状态过程中的思考点
android·架构·android jetpack
前行的小黑炭14 小时前
Android 关于状态栏的内容:开启沉浸式页面内容被状态栏遮盖;状态栏暗亮色设置;
android·kotlin·app
用户0918 小时前
Flutter构建速度深度优化指南
android·flutter·ios
PenguinLetsGo18 小时前
关于「幽灵调用」一事第三弹:完结?
android
雨白1 天前
Android 多线程:理解 Handler 与 Looper 机制
android
sweetying1 天前
30了,人生按部就班
android·程序员
用户2018792831671 天前
Binder驱动缓冲区的工作机制答疑
android
真夜1 天前
关于rngh手势与Slider组件手势与事件冲突解决问题记录
android·javascript·app