Android从屏幕刷新到View的绘制(一)之 Window、WindowManager和WindowManagerService之间的关系

news2024/11/25 2:24:46

0. 相关分享

Android从屏幕刷新到View的绘制(一)之 Window、WindowManager和WindowManagerService之间的关系
Android从屏幕刷新到View的绘制(二)之Choreographer、Vsync与屏幕刷新

1. 相关类

WindowManagerService,下文简称WMS

这是一个系统服务,由SystemServer启动,运行在一个Binder线程,管理着Android系统中所有的Window。

这有什么实际作用呢?除了刷新View,它还可以为其他服务提供Window管理的支持,例如当触摸屏幕时产生输入事件,InputManangerService可以通过WMS来拿到所有Window信息,找到合适的Window进行输入事件的派发,此后,Window就会把这个输入事件传递给顶级View,也就是DecorView,接着就进入到熟悉的事件分发机制了。

Window

表示一个窗口的抽象的概念,这是一个空实现的抽象类,在APP进程中,它有实现类PhoneWindow。

PhoneWindow

是Window的实现类,一个Activity对应着一个PhoneWindow,在PhoneWindow中有一个顶级View——DecorView

DecorView

Activity的根View,继承自FrameLayout

ViewRootImpl

负责DecorView下所有View的调度,例如invalidate()等,Activity下的所有View都会向上找到DecorView,最终找到ViewRootImpl来处理

WindowManagerImpl

它是WindowManager接口的实现类,WindowManager接口又继承自ViewManager,顾名思义它是管理View和Window关系的。ViewManager中规范了三个方法:addView(), updateViewLayout(), removeView()。这三个任务最后也交到了WindowManagerGlobal来处理

WindowManagerGlobal

它是一个单例设计,一个APP进程对应一个WindowManagerGlobal,持有WMS的binder引用,可以通过它来与WMS进行IPC(跨进程通信)交互。

它还拥有许多集合,例如mViews包含了进程下所有View,mRoots包含了进程下所有ViewRootImpl,mDyingViews包含了进程下所有要销毁的View。

2. 上述类间的关系图

Window是View的载体,我们想要对Window进行添加、删除、更新View,就要通过WindowManager,实际管理着是WindowManagerGlobal,它与WMS通过Session进行IPC通信,具体的实现交给了WMS处理。

VeznzR.png

WMS也会为每个WIndow创建一个WindowState来管理它们,具体的渲染工作交给了SurfaceFinger处理。本文只讨论View、Window、WindowManager与WMS的关系。

img

3. Window对View的管理

Window是抽象的概念,它的实现类为PhoneWindow,内部维护着一个DecorView,换句话说,WIndow是以View的形式呈现给用户的。Window对View的操作,实际是通过ViewRootImpl实现。使用过程中,我们不会接触并访问到Window,而是通过WindowMananger来进行操作。

接下来说的Window对View的管理其实具体来说是对DecorView的管理,一个Window对应一个DecorView。其中DecorView的创建、删除,就相当于Window的添加、删除,所以也有的地方说,这部分的讨论叫做window的创建、更新、删除。

3.1 Window对View的添加

WindowManager的实现类是WindowManagerImpl

//WindowManagerImpl
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
    mGlobal.removeView(view, false);
}

WindowManagerImpl将对View的添加、删除、更新都交给了WIndowManagerGlobal,mGlobal是一个单例:

//WindowManagerGlobal
public static WindowManagerGlobal getInstance() {
    synchronized (WindowManagerGlobal.class) {
        if (sDefaultWindowManager == null) {
            sDefaultWindowManager = new WindowManagerGlobal();
        }
        return sDefaultWindowManager;
    }
}

首先我们看到WindowManagerGlobal的addView(),具体步骤大概如下:

  1. 各类数据检查
  2. 更新mViews、mRoots等集合
  3. 创建一个ViewRootImpl,将要添加的view交给它
public void addView(View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) {
    //1.数据检查
    //...
 
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        //2. 更新mViews/mRoots等集合
        root = new ViewRootImpl(view.getContext(), display);
		
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        //3.把要添加的view交给ViewRootImpl
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            //...
        }
    }
}

ViewRootImpl添加到View之后,主要做了几件事:

  1. 调用requestLayout()异步刷新view
  2. 通过session与WMS通信,真正完成window的添加
//ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        //值维护一个view(DecorView)
        if (mView == null) {
            mView = view;
            int res;
            //1. 调用requestLayout绘制View
            requestLayout();
            //...
            try {
                //通过session与WMS通信,完成window的添加
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                                  getHostVisibility(), mDisplay.getDisplayId(),
                                                  mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                                  mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {...}
            //...
}

首先,requestLayout()最后通过scheduleTraversals()来申请绘制,这部分我们在屏幕绘制的部分再详谈。只需要知道发起了重绘View的请求即可。

//ViewRootImpl
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //检查线程
        checkThread();
        //标志位
        mLayoutRequested = true;
        //请求绘制
        scheduleTraversals();
    }
}

当收到允许绘制的通知的时候,最终会进入到performTraversals(),从而对DecorView自顶向下地分发绘制流程,如measure/layout/draw。

requestLayout()之后,ViewRootImpl通过session通知WMS,去完成window的添加。IWindowSession是一个Binder引用,可以通过它来与WMS通信。WMS为每个应用创建一个单独的session,这个session可以通过WindowManagerGlobal获得。

public ViewRootImpl(Context context, Display display) {
        mContext = context;
    	//实例化ViewRootImpl的时候,ViewRootImpl就从WIndowManagerGlobal中拿到了可用的session
        mWindowSession = WindowManagerGlobal.getWindowSession();
	...
}

看一下WindowManagerGlobal中如何提供session的:

public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        //单例,如果已经有了,就不再创建了
        if (sWindowSession == null) {
            try {
                InputMethodManager imm = InputMethodManager.getInstance();
                //WMS的引用,这个是全局引用,和AMS一样,在ServiceManager中获取。
                IWindowManager windowManager = getWindowManagerService();
                //wms创建一个session交给当前客户端进程的WindowManagerGlobal。
                sWindowSession = windowManager.openSession(
                    new IWindowSessionCallback.Stub() {
                        @Override
                        public void onAnimatorScaleChanged(float scale) {
                            ValueAnimator.setDurationScale(scale);
                        }
                    },
                    imm.getClient(), imm.getInputContext());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return sWindowSession;
    }
}

首先我们发现WindowManagerGlobal首先会拿到WMS的引用,然后才通过WMS创建一个session,用于后续的通信。我们知道,系统服务由ServiceMananger管理,可以全局获取到WMS的binder引用。但为了让WMS知道和它通信的到底是哪个window,这久需要单独创建一个session,客户端window通过session向WMS发起通信。

我们来看一下WMS是如何处理这个openSession()请求的:

//WindowManagerService
@Override
public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
                                  IInputContext inputContext) {
    //实例化了一个Session
    Session session = new Session(this, callback, client, inputContext);
    return session;
}

Session是一个binder实体,它持有WMS的直接引用,客户端window可以通过session来间接地通知WMS做一些操作。

获取到session后,ViewRootImpl的setView()就进入到了最后一步:session.addToDisplay():

//Session
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
                        int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
                        Rect outStableInsets, Rect outOutsets,
                        DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
    //mServices就是WMS,让WMS来addWindow()
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                              outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel);
}

我们来到WMS的addWIndow()

//WindowManagerService
public int addWindow(Session session,IWindow client,...){
    //为session建立一个WindowState,可以通过这个WindowState来与客户端通信。
    final WindowState win = new WindowState(this, session, client, token, parentWindow,
                                            appOp[0], seq, attrs, viewVisibility, session.mUid,
                                            session.mCanAddInternalSystemWindow);
}

至此,APP进程就成功将view注册到WMS中,同时,APP进程的WindowManagerGlobal可以通过session对WMS进行binder通信,WMS也可以通过WindowState来与WindowManagerGlobal进行binder通信。

3.2 Window对View的更新

我们再看到mGlobal.updateViewLayout(view,params);

//WindowManagerGlobal
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    //1. 参数检查
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
	//2.更新layoutParams以及mRoot中对应的ViewRootImpl
    view.setLayoutParams(wparams);
    synchronized (mLock) {
        //找到这个view对应的ViewRootImpl是谁
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        //更新ViewRootImpl,这里setLayoutParams()最后也会调用到scheduleTraversals()来请求重绘,细节不在这里讨论
        root.setLayoutParams(wparams, false);
    }
}

最后到了ViewRootImpl.setLayoutParams(),最后也会调用到scheduleTraversals()来请求重绘,View的绘制、刷新的细节不在本文中讨论。

3.3 WIndow对View的删除

对View的删除大概分为以下几步:

  1. 首先让ViewRootImpl用die来删除
  2. 然后将要删除的view记录到mGlobal的mDyingViews集合中。
  3. View可能立即删除 doDie(),也可能不是立即删,就放入队列
  4. 移除各种回调
  5. 最后通知WMS移除这个window

我们直接看到最后:

//ViewRootImpl
mWindowSession.remove(mWindow);

4. 都有哪些Window会进行这样的创建、更新、删除操作?

Activity、Dialog、Toast等都需要View,而View都需要依附于WIndow

先从简单的Dialog对Window的创建谈起,最后再长篇大论到Activity的Window的创建

4.1 Dialog 的window创建

Dialog的构造方法明显看打了PhoneWindow的实例化、WindowManager的引用(借以与WMS通信)

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
	//...
    //获取windowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //实例化PhoneWindow
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    //设置回调
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    //...
}

接着来到setContentView,就是把视图布局交给DecorView,细节我们在Activity.ssetContent()中讨论,几乎一样的。我们再来看一下show()方法

public void show() {
    //...
    mDecor = mWindow.getDecorView();
    //...
    WindowManager.LayoutParams l = mWindow.getAttributes();
    boolean restoreSoftInputMode = false;
    if ((l.softInputMode
         & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
        l.softInputMode |=
            WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        restoreSoftInputMode = true;
    }
    //使用WindowManager.addView
    mWindowManager.addView(mDecor, l);
    //...
}

mWindowManager.addView()我们知道最后会通过session通知到WMS,需要创建一个window来展示这个dialog。

4.2 长篇大论 Activity 的 Window创建

大概步骤如下:

  1. APP进程启动时,会通知AMS,application启动好了,并把applicationThread这个binder实体引用交给AMS
  2. AMS再以此通知app进程的第一个activity启动
  3. Activity启动之前初始化WindowManagerGlobal
  4. 最后onResume()执行完毕后,将window的添加通知给WMS

我们直接切入重点,AMS -> ActivityStarter-> ActivityStackSupervisor->realStartActivity()->app进程->handleLaunchActivity(),在这个方法中主要调用了Activity几个回调:

  1. attach()
  2. onCreate()
  3. onStart()
  4. onResume()
//ActivityThread
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
    // Initialize before creating the activity
    if (!ThreadedRenderer.sRendererDisabled) {
        GraphicsEnvironment.earlyInitEGL();
    }
    //创建单例,创建WMS引用 IWindowManager
    WindowManagerGlobal.initialize();
	//启动activity
	performLaunchActivity();
    //回调onResume()
    handleResumeActivity();
}

Activity.attach()主要做了几件事:

  1. 建立了一个与Activity一一对应的PhoneWindow实例mWindow
  2. 为mWindow设置一个WMS引用
  3. Activity的mWindowManager也持有WMS引用
public class Activity implements Window.Callback,Window.OnWindowDismissedCallback,WindowControllerCallback,...{

    private Window mWindow;//一个Activity有一个Window,实现类为PhoneWindow
    
    private WindowManager mWindowManager;//WMS的引用

    final void attach(...){
        //一个activity对应一个PhoneWindow
        mWindow = new PhoneWindow(Activit.this,window,...);
        //设置一些回调
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        //...
        //给Window设置一个WMS的引用
        mWindow.setWindowManager(
            //获取WMS的远程引用
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        
        //拿到WMS的引用
        mWindowManager = mWindow.getWindowManager();
    }
}

正常情况下,我们可能会在onCreate()中调用setContentView():

public void setContentView(int layoutResId){
    getWindow().setContentView(layoutResId);
    initWindowDecorActionBar();
}

getWindow()拿到的是mWindow,实现类是PhoneWindow,看到它的setContentView()

//PhoneWindow
private DecorView mDecor;
private ViewGroup mContentParent;

public void setContentView(int layoutResId){
    if(mContentParent==null){
        installDecor();//创建decorView,如果没有的话
    }
    //...
}

private void installDecor(){
    //1. mDecore初始化
    if(mDecor==null){
        mDecore = generateDecor(-1);//new DecoreView(phoneWindow.this)
    }
    //2. mContentParent初始化,mDecoreView下的第一个ViewGroup
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);//根据xml属性配置Activity的默认版型样式
    }

}

protected ViewGroup generateLayout(DecorView decor){
    //...
    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    //设置背景
    //...
    //设置title之类的
    mDecor.finishChanging();
}

Activity的DecorView是由PhoneWindow来管理的,包括mContentParent。最后来到handleResumeActivity(),需要注意的是:

  1. 首先回调onResume()
  2. 然后使用设置了的DecorView,或者给一个默认的DecorView,通知WMS要addView()

performResumeActivity()->Activity.performResume()->Activity.onResume()

//ActivityThread
final void handleResumeActivity(){
    //1.回调onResume
    r = performResumeActivity();
    //2.如果之前没有通过setContentView()设置mDecor,则给一个默认的
    Activity a = r.activity;//当前这个activity
    if(r.window==null){
        //如果activity在onResume之后还没有mDecor,则会在这里给一个
        View decor = r.window.getDecorView();//如果没有的话会PhoneWindow.installDecor()
        decor.setVisibility(View.INVISIBLE);
        //这个wm是一个WindowManager,它持有WMS的引用
        //WindowManager也是一个接口,实现类是WindowManagerImpl
        ViewManager wm = a.getWindowManager();
        a.mDecor = decor;
        if(a.mVisibleFromClient){
            if(!a.mWindowAdded){
                a.mWindowAdded= true;
                wm.addView(decor,l);//l:WM.LayoutParams
            }
        }   
    }
}

如果在onResume()结束之前,用户都没调用setContentView(),那么会在这里给到一个mDecor。

wm.addView(decor,l)-> WindowManagerGlobal.addView()

此后的工作我们之前就讨论过了,不过是创建一个ViewRootImpl来维护这个DecorView,请求绘制之后通过session让WMS来添加window。

Activity.onDestroy()时,进入到ActivityThread.handleDestroyActivity(),其中通知了WMS去removeView(),也就是移除这个Activity的Window。最后再通知AMS自己的销毁,ActivityManager.getService().activityDestroyed();

参考文献:

本文基于Android8.0源码分析。结合一些博客的思路进行编排。
https://juejin.cn/post/6863756420380196877
https://blog.csdn.net/hfy8971613/article/details/103241153

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux安装Redis步骤

1 下载安装包并解压 官网:https://download.redis.io 下载安装包: wget https://download.redis.io/redis-stable.tar.gz 解压 tar -zxvf redis-stable.tar.gz* 2 安装 安装 cd redis-stable make install PREFIX/opt/install/redis6 设置环境变量 vi …

Python学习-----项目设计1.0(设计思维和ATM环境搭建)

目录 前言: 项目开发流程 MVC设计模式 什么是MVC设计模式? ATM项目要求 ATM项目的环境搭建 前言: 我个人学习Python大概也有一个月了,在这一个月中我发布了许多关于Python的文章,建立了一个Python学习起步的专栏…

企业级信息系统开发学习1.3——利用注解配置取代Spring配置文件

文章目录一、利用注解配置类取代Spring配置文件(一)打开项目(二)创建新包(三)拷贝类与接口(四)创建注解配置类(五)创建测试类(六)运行…

史上最经典垃圾回收器(CMS,G1)详解、适用场景及特点、使用命令

文章目录垃圾收集器介绍总结各个垃圾收集器之间的关系垃圾收集器使用命令及默认值详解各个垃圾收集器SerialParNewParallel ScavengeSerial OldParallel OldCMS(Concurrent Mark Sweep)G1(Garbage First)适用场景及推荐垃圾收集器介绍总结 垃圾收集器可以帮助我们进行具体的垃…

HDFS优化

单节点多块磁盘数据均衡 生成HDFS块均衡计划 hdfs diskbalancer -plan node1 执行均衡计划,node1.plan.json均衡计划文件 hdfs diskbalancer -execute node1.plan.json 查看当前均衡任务的执行情况 hdfs diskbalancer -query node1 取消均衡任务hdfs diskbalancer -cancel nod…

(三十九)undo log版本链是个什么东西?

今天我们正式开始切入讲解MySQL中多个事务并发执行时的隔离到底是怎么做的,因为我们知道默认是骚气的RR隔离级别,也就是说脏写、脏读、不可重复读、幻读,都不会发生,每个事务执行的时候,跟别的事务压根儿就没关系&…

移动web基础

初始缩小&#xff1a;布局视口大于视觉视口 初始放大&#xff1a;布局视口小于视觉视口 布局视口等于视觉视口&#xff08;这种动作行为叫做理想视口&#xff09; <meta name"viewport" content"width375" /> <meta name"viewport"…

云原生|kubernetes|网络插件flannel二进制部署和calico的yaml清单部署总结版

前言&#xff1a; 前面写了一些关于calico的文章&#xff0c;但感觉好像是浅尝辄止&#xff0c;分散在了几篇文章内&#xff0c;并且很多地方还是没有说的太清楚云原生|kubernetes|kubernetes的网络插件calico和flannel安装以及切换_calico换flannel_晚风_END的博客-CSDN博客 …

在C#中使用信号量解决多线程访问共享资源的冲突问题

目前在我写的233篇原创文章中&#xff0c;有两篇是粉丝可见的&#xff0c;其中《C#线程的参数传递、获取线程返回值以及处理多线程冲突》这篇文章有179个粉丝关注&#xff0c;看到不断有人关注这篇文章&#xff0c;这表明学习C#的人还是挺多的&#xff0c;感觉文章内容不够厚实…

泛型<E>

泛型 案例引出泛型 按要求写出代码&#xff1a; 在ArrayList中添加3个Dog对象&#xff0c;Dog对象有name和age两个属性&#xff0c;且输出name和age public class test1 {public static void main(String[] args) {ArrayList list new ArrayList();list.add(new Dog(10,&quo…

Python解题 - CSDN周赛第32期 - 运输石油(三维背包)

上期周赛因为最后一题出现bug&#xff0c;再加上都是经典的模板题&#xff0c;问哥就懒得写题解了。 本期也是有两道考过的题目&#xff0c;不过最后一题因为考到了背包问题的特殊类型&#xff0c;还是值得拿出来记个笔记。 第一题&#xff1a;传奇霸业 传奇霸业&#xff0c;是…

Unity高程图生成

序大概就是根据一个灰度图&#xff0c;生成一个地形。分两步来实现吧&#xff1b;首先&#xff0c;用随机数生成地形&#xff1b;然后&#xff0c;根据灰度图生成地形。小白&#xff0c;没啥基础&#xff0c;所以只能慢慢来。参考&#xff1a;【萌新图形学】地形网格生成入门 含…

基于stm32电梯管理系统设计

基于stm32电梯管理系统设计这里记录一下以前自己做的嵌入式课程设计&#xff0c;报告中的图片和文字太多了&#xff0c;全部一个一个把搬过来太麻烦了,需要完整文本和代码自行q我963160156&#xff0c;也可在微信公众号 *高级嵌入式软件* 里回复 *电梯* 查看完整版文章摘要关键…

Oracle Apex 21.2 安装过程

什么是 Oracle APEX&#xff1f; Oracle APEX 是广受欢迎的企业级低代码应用平台。借助该平台&#xff0c;您可以构建功能先进的可扩展安全企业应用&#xff0c;并在任何位置&#xff08;云或内部部署&#xff09;部署这些应用。 使用 APEX&#xff0c;开发人员可快速开发并部…

域组策略自动更新实验报告

域组策略自动更新实验报告 域组策略自动更新实验报告 作者: 高兴源 1要求、我公司为了完善员工的安全性和系统正常漏洞的维护&#xff0c;所以采用域组策略自动更新的方法来提高账户安全性&#xff0c;减少了用户的错误。 1.实验环境如下1台2008r2一台创建域&#xff0c;一台wi…

【云原生】k8s中Pod进阶资源限制与探针

一、Pod 进阶 1、资源限制 当定义 Pod 时可以选择性地为每个容器设定所需要的资源数量。 最常见的可设定资源是 CPU 和内存大小&#xff0c;以及其他类型的资源。 当为 Pod 中的容器指定了 request 资源时&#xff0c;调度器就使用该信息来决定将 Pod 调度到哪个节点上。当还…

嵌入式 STM32 步进电机驱动,干货满满,建议收藏

目录 步进电机 1、步进电机驱动原理 2、步进电机驱动 3、步进电机应用 1、第一步&#xff1a;初始化IO口 2、设置行进方式 四、源码 步进电机 步进电机被广泛应用于ATM机、喷绘机、刻字机、写真机、喷涂设备、医疗仪器及设备、计算机外设及海量存储设备、精密仪器、工业…

_improve-3

createElement过程 React.createElement()&#xff1a; 根据指定的第一个参数创建一个React元素 React.createElement(type,[props],[...children] )第一个参数是必填&#xff0c;传入的是似HTML标签名称&#xff0c;eg: ul, li第二个参数是选填&#xff0c;表示的是属性&#…

String、StringBuffer和StringBuilder的详解

目录 一、String讲解 1.String&#xff08;String字符串常量&#xff09; 2.String 拼接方式与性能的影响 二、StringBuffer 和 StringBuilder 讲解 1.StringBuffer 和 StringBuilder 使用场景:(StringBuffer、StringBuilder字符串变量) 2.StringBuffer的使用 3.StringB…

shell脚本常用命令

shell概述 shell是一个命令行解释器&#xff0c;它接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 shell还是一个功能强大的编程语言&#xff0c;易编写、易调试、灵活性强。 shell解析器 查看系统自带的所有shell解析器 cat /etc/shells查看系统默认的shell解析…