【iOS】—— RunLoop和多线程相关问题总结

news2024/12/24 8:39:30

RunLoop

1. 讲讲RunLoop,项目中有用到过吗?

RunLoop 的基本作用:保持程序的持续运行,节省 CPU 的资源,提高程序的性能 ( 没有事情,就请休眠,不要功耗。有事情,就处理)。
简单举个例子,如果用Xcode的Command Line Tool文件来写OC,在代码里创建一个NSTimer,它并不能正常运行,因为这个程序和APP不同,程序运行一次直接结束,这时候我们需要把Runloop给Run起来,NSTimer就能用了。

2. RunLoop内部实现逻辑

请添加图片描述

3. runloop与线程的关系

  • 每条线程,都有唯一的一个,与之对应的 RunLoop 对象
  • runloops[thread] = runloop
    RunLoop 保存在一个全局的 Dictionary 里,线程作为 Key, RunLoop 作为 Value
  • 线程刚创建时,并没有 RunLoop 对象, RunLoop 会在第一次获取它时创建
  • RunLoop 会在线程结束时,销毁
  • 主线程的runloop对象是默认就存在且默认是打开的,其他线程的runloop对象只有在第一次获取runloop对象时才会创建而且默认是关闭状态,需要我们手动调用run方法运行。

4. timer与runloop的关系

我们创建timer时,之所以timer能运行,是因为创建timer时,一般情况下,是在主线程中创建,这时会默认将timer以defaultRunloopModel的类型加入主线程,而主线程的runloop对象默认是打开的,从而timer可以运行。

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以以下两段代码含义相同:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

对于NSTimer在滑动时停止工作的问题

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
  • 当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
  • 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

想要解决这个问题也很简单,我们直接让NSTimer在两种mode下都能工作就完了,这就用到我们之前不太清楚其用法的NSRunLoopCommonModes了:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

当然你也可以把NSTimer分别加入到NSDefaultRunLoopModeUITrackingRunLoopMode,这两种写法是相同的,因为系统的mode是默认在_commonModes中的:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

5. 如何解决timer不准确的问题

可能造成NSTimer不准确的原因:

  • RunLoop循环过程中,被NSTimer触发事件阻塞了,导致循环不能及时进行下去,延误之后NSTimer触发时间。
  • RunLoop循环过程中,在某一时刻主线程发生了阻塞情况,导致循环不能及时进行下去,厌恶NSTimer触发时间。
  • RunLoop循环过程中,发生了模式的转换,(比如UIScrollView的滑动) 导致原有模式下的NSTimer不会正常触发。

以上情况都是由于NSTimer所依赖的run loops会被多种原因干扰正常循环,所以要想解决NSTimer精度问题,就要避免所依赖的run loops被外界干扰。

  • 注意:虽然第三种情况可以指定NSTimer所处模式为NSRunLoopCommonModes,但是这种解决方法并不能改变run loops在特定模式下不能处理其余模式事件的本质。

终极解决办法:

  • 尽量避免将NSTimer放入容易受到影响的主线程run loops中。
  • 尽量避免将耗时操作放入NSTimer依赖的线程中。
  • 尽量避免在NSTimer触发事件中进行耗时操作,如果不能避免,将耗时操作移至其余线程进行。

6. RunLoop是怎么响应用户操作的,具体流程是怎么样的

Objective-C 中,RunLoop 是一个事件循环机制,用于处理用户操作、定时器事件、网络请求等异步任务RunLoop 的主要作是监听事件并分发给相应的处理器。

下面是 RunLoop 响应用户操作的大致流程:

  • 当用户进行某个操作(例如点击按钮)时,操作系统会将该事件发送给应用程序。
  • 应用程序主线程会收到这个事件,并将其添加当前线程的 RunLoop 中。
  • RunLoop 开始运行,并进入一个循环状态,不断地检查是否事件需要处理。
  • 如果有事件需要处理,RunLoop 会将事件从队列中取出,并将其分发给相应的处理器(例如事件响应方法)。
  • 处理器执行相应的代码处理事件,可能包括更新用户界面、执行业务逻辑等操作。
  • 处理完事件后,RunLoop 继续循环,等下一个事件的到来。
  • RunLoop 的关键是在循环中不断检查事件,并及时分发给处理器这样可以保证用户操作的响应速度,并且免阻塞主线程。

需要注意的是RunLoop 并不是只处理用户操作,它还负责处理其他步任务,如定时器事件、网络请求等。RunLoop 的设计目的是为了提供一种效的事件处理制,使应用程序能够时响应用户操作其他异步任务,同时保持界面的畅性。

7. 说说RunLoop的几种状态

五种状态:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到
  • kCFRunLoopCommonModes:并不是一种模式
    只是一个标记,当mode标记为common时,将mode添加到runloop中的_commonModes中。runloop中的_commonModes实际上是一个Mode的集合,可使用CFRunLoopAddCommonMode()将Mode放到_commonModes中。每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的同步到具有Common标记的所有的Mode

8. RunLoop的mode有什么作用

用来控制一些特殊操作只能在指定模式下运行,一般可以通过指定操作的运行mode 来控制执行时机,以提高用户体验。
系统默认注册了 5 个 Mode

  • kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode下运行,对应 OC 中的:NSDefaultRunLoopMode
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • kCFRunLoopCommonModes:这是一个标记 Mode,不是一种真正的 Mode,事件可以运行在所有标有 common modes 标记的模式中,对应 OC 中的NSRunLoopCommonModes , 带 有 common modes 标 记 的 模 式 有 :UITrackingRunLoopModekCFRunLoopDefaultMode
  • UIInitializationRunLoopMode:在启动 App 时进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到

AutoreleasePool

在RunLoop这块讲到了很多AutoreleasePool的知识,在这补充一下。
自动释放池的PUSH流程:
请添加图片描述
自动释放池的POP流程:
请添加图片描述

1.临时变量什么时候释放?

  • 如果在正常情况下,一般是超出其作用域就会立即释放
  • 如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放

2.AutoreleasePool原理

  • 自动释放池的本质是一个AutoreleasePooIPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接
  • 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的obic autoreleasePoolPudhobic_autoreleasePoolPop,实际上是调用AutoreleasePoolPagepushpop两个方法
  • 每次调用push操作其实就是创建一个新的AutoreleasePooIPage,而AutoreleasePoolPage的具体操作就是插入一个POOL BOUNDARY,并返回插入POOL BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况
    • page存在,且不满时,调用add方法将对象添加至pagenext指针处,并next递增
    • page存在,且已满时,调用autoreleaseFulIPage初始化一个新的page,然后调用add方法将对象添加至page栈中
    • page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
  • 当执行pop操作时,会传入一个值,这个值就是push操作的返回值,即POOL_BOUNDARY的内存地址token,所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release释放token之前的对象,并把next指针到正确位置

3.AutoreleasePool能否嵌套使用?

  • 可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高
  • 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的
  • 自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,在释放外面的

4.哪些对象可以加入AutoreleasePool?alloc创建可以吗?

  • MRC下使用newalloccopy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中
  • MRC下设置为autorelease的对象不需要手动释放,会直接进入自动释放池
  • 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中
  • ARC下只需要关注引用计数,因为创建都是在主线程进行的,系统会自动为主线程创建AutoreleasePool,所以创建会自动放入自动释放池

5.AutoreleasePool的释放时机是什么时候?

在没有手动添加AutoreleasePool的情况下,Auturelease对象是在当前的runloop迭代结束的时候进行释放;而它能否释放的原因是系统在每个runloop的迭代中都加入了自动释放池的push和pop。

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
  • 第一个0bserver监视的事件是Entry即将进入 Loop,其回调内会调用_objc_autoreleasePooIPush()创建自动释放池,其order是-2147483647,优先级最高保证创建释放池发生在其他所有回调之前。
  • 第二个Observer监视了两个事件:BeforeWating(准备进入休)时调用 obic_autoreleasePoolPop()obic autoreleasePooPush释放旧的池井创建新池; ·Exit·即将很出Loop)时调用 obic_autoreleasePooIPop(来释放自动释放池。这个Obsererorder是 2147483647优先级最低,保证其释放池子发生在其他所有回调之后。

6.thread和AutoreleasePool的关系

  • 每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
  • 新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
  • 对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池

总结:每个线程都有与之关联的自动释放池堆栈结构。新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈。对于当前线程来说。释放对象会被压栈到栈顶线程停止时,会自动释放与之关联的自动释放池

7.RunLoop和AutoreleasePool的关系

  • 主程序的RunLoop在每次事件循环之前,会自动创建一个AutoreleasePool
  • 并且会在事件循环结束时,执行drain操作,释放其中的对象。

多线程

1. 你理解的多线程

好处:

  • 1.使用线程可以把占据时间长的程序中的任务放到后台去处理
  • 2.用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以d出一个进度条来显示处理的进度
  • 3.程序的运行速度可能加快
  • 4.在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。

缺点:

  • 1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
  • 2.更多的线程需要更多的内存空间。
  • 3.线程的中止需要考虑其对程序运行的影响。
  • 4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。

2. iOS多线程的方式有哪几种,你更倾向于哪一种

实现多线程的方法:

请添加图片描述
更倾向于GCD和NSOperation,封装更高级,使用起来更方便。

3. 有用过GCD吗?

  • 1.在其他线程请求完数据,在主线程刷新UI
    //让处理在主线程中执行
	dispatch_async(dispatch_get main_queue(), ^{
		/*
		 *只在主线程可以执行的处理
		 *例如用户界面更新
		 */
	});
  • 2.Manager封装网络请求时候,可以用GCD实现单例
- (void)once {        //GCD的一次性代码(只执行一次)
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行1次的代码(这里面默认是线程安全的)
    });
}
  • 3.GCD的定时器
- (void)after {
    NSLog(@"run -- 0");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2秒后异步执行这里的代码...
       NSLog(@"run -- 2");
    });
}
  • 4.信号量加锁保证线程安全
    dispatch_semaphore_t semalook = dispatch_semaphore_create(1);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semalook, DISPATCH_TIME_FOREVER);
        NSLog(@"1开始");
        sleep(2);
        NSLog(@"1结束");
        dispatch_semaphore_signal(semalook);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semalook, DISPATCH_TIME_FOREVER);
        NSLog(@"2开始");
        sleep(2);
        NSLog(@"2结束");
        dispatch_semaphore_signal(semalook);
    });

4. GCD的队列类型

可以使用dispatch_queue_create来创建对象,需要传入两个参数,第一个参数表示队列的唯一标识符,用于DEBUG,可为空;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。

// 串行队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);

5. 说一下OperationQueue和GCD的区别,以及各自的优势

区别

  1. GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
  2. GCD只支持FIFO的队列而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
  3. NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
  4. NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld
  • 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选
  • 如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持
  • 不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

6. 线程安全的处理手段有哪些?

  • 互斥锁(Mutex):使用互斥锁可以确保同一时间只有一个线程访问共享资源。在Objective-C中,可以使用@synchronized关键字来创建互斥锁。
@synchronized (self) {
    // 访问共享资源的代码
}
  • 自旋锁(Spin Lock):自旋锁一种忙等待的锁,它会不断地尝试获取锁,直到成功为止。在Objective-C中,可以使用os_unfair_lock来创建自旋锁。
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(lock);
// 访问共享资源的代码
os_unfair_lock_unlock(lock);
  • 信号量(Semaphore):信量是一种数器,用于控制同时访问某个资源的线程数量。在Objective-C中可以使用dispatch_semaphore来创建信号量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
dispatch_semaphore_signal(semaphore);
  • 原子操作(Atomic Operations):原子操作是一种特的操作,可以确保对共享资源的读写操作是原子的,即不会被其他线程中断。在Objective-C中,可以使用atomic键字来声明属性原子类型。
@property (atomic, strong) NSObject *sharedObject;
  • 串行队列(Serial Queue):使用串行队列可以确保任务按顺序执行,从而避多个线程同时访问共享资源。可以使用GCD(Grand Central Dispatch)来创建串行队列。
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    // 访问共享资源的代码
});

7. OC的锁有哪些

【iOS】—— iOS中的相关锁

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

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

相关文章

如何制定数据采集解决方案?

数据采集仍是人工智能(AI)构建团队的主要瓶颈。原因各不相同:用例数据可能不足,深度学习等新机器学习(ML)技术需要更多数据,或者团队并未建立获取所需数据的适当流程。但无论如何,对…

xmind latex【记录备忘】

xmind latex 换行 换行必须要有\begin{align}和\end{align},此时再在里面用\才能换行,如果只写112\224是不能换行的

2023第五届全国生物资源提取与应用创新论坛即将举办

01、会议背景 为进一步加强生物资源提取行业交流与合作,促进业“产学研用”融合,提升行业科技创新水平,增强行业国际竞争力,中国生物发酵产业协会、浙江科技学院、浙江工业职业技术学院、浙江省农业生物资源生化制造协同创新中心&…

JS算法之递归

含义 递归函数是指能够直接或间接调用自身的方法或函数。 // 直接 function do() {do(); }// 间接 function do() {do2(); } function do2() {do() } 每个递归函数必须有基线条件(即停止点,一个不再递归调用的条件。)否则将无限递归下去。 因…

每天一个电商API分享:淘宝/天猫获取商品销量详情 API(月销总销)

淘宝/天猫获取商品销量详情API(月销总销)是一种用于获取电商平台上商品销量数据的接口。通过这个API,用户可以方便地获取到商品的月销量以及总销量等详细信息,快速了解商品的销售情况。 随着电商行业的快速发展,不少卖…

pytest study

pytest 测试用例的识别与运行 测试文件:test_*.py 和 *_test.py 以test开头或结尾的文件 测试用例:Test*类包含的所有 test_*的方法(测试类不能带有__init__方法), 不在class中的所有test_*的方法 def func(x):r…

RunnerGo性能测试怎么做?

性能测试—计划管理 新建计划: 在左侧导航栏中,选择“性能测试”,点击“计划管理”进入计划管理界面,点击右上角新建计划来新建测试计划。任务类型选择后不可再更改。定时模式下, 该计划内最多只能创建一个场景。 普通任务&…

如何往MySQL中插入100万条数据?

需求 现在有一个 数据量 为100万的数据样本 100w_data.sql 其数据格式如下,截取最后十条数据 999991,XxGdnLZObA999991,XxGdnLZObA,XxGdnLZObA,2020-3-18,1 999992,TBBchSKobC999992,TBBchSKobC,TBBchSKobC,2020-9-8,2 999993,rfwgLkYhUz999993,rfwgLkYhUz,rfwgLk…

APISIX 安全评估

背景 有大佬已经对 [apisix攻击面](https://ricterz.me/posts/2021-07-05-apache-apisix-attack- surface-research.txt)做过总结。 本文记录一下自己之前的评估过程。 分析过程 评估哪些模块? 首先我需要知道要评估啥,就像搞渗透时,我得…

【三维重建】【深度学习】Windows10下NeRS官方代码Pytorch实现

【三维重建】【深度学习】Windows10下NeRS官方代码Pytorch实现 提示:最近开始在【三维重建】方面进行研究,记录相关知识点,分享学习中遇到的问题已经解决的方法。 文章目录 【三维重建】【深度学习】Windows10下NeRS官方代码Pytorch实现前言NeRS模型运行下载源码并安装环境安装…

体制内裸辞,她用云端地球实现了自己的乡村梦

追逐田园的“诗与远方” “我最初的梦想,就是有一个亲手打造的、能装进个人喜好的小院子。”为完成自己的梦想,吕春萍毅然放弃了体制内的工作,来到秦岭脚下的桥南镇曹峪村,践行自己的“乡村梦”。 起初,吕春萍做了五…

「开源项目」开源企业级问答系统-Danswer

danswer 基本介绍 开源企业级问答系统,可以对内部文档进行自然语言提问,并返回可靠的答案、引用和参考资料,可以连接到多种常见工具,如Slack、GitHub和Confluence。 在线预览 暂无在线预览地址,不过可以自行部署使用…

【error】svn 清理以下路径失败 原始内容不存在

前言 目前我们这边的内网代码是通过 TortoiseSVN 进行版本管理的,平时用着也挺好的,没碰到什么大问题。 但是,今天碰到了一个比较棘手的问题,在这里做一下记录,以方便自己和有需要的朋友在之后碰到该类问题时有个参考…

[php-cos]ThinkPHP项目集成腾讯云储存对象COS

Cos技术文档 1、安装phpSdk 通过composer的方式安装。 1.1 在composer.json中添加 qcloud/cos-sdk-v5: >2.0 "require": {"php": ">7.2.5","topthink/framework": "^6.1.0","topthink/think-orm": "…

为什么中小企业数字化转型这么难?_光点科技

随着科技的飞速发展和数字化时代的到来,数字化转型已成为现代企业发展的必然趋势。大型企业普遍拥有雄厚的资源和资金,能够较为顺利地进行数字化转型。然而,对于中小企业来说,数字化转型却面临着诸多挑战和困难。 资金限制&#x…

5个步骤完成Linux 搭建Jdk1.8环境

1:首先,在Linux系统中创建一个目录,用于存放JDK文件。可以选择在/opt目录下创建一个新的文件夹,例如/opt/jdk。 sudo mkdir /opt/jdk 2:将下载的jdk-8u381-linux-x64.tar.gz文件复制到新创建的目录中。 sudo cp jdk…

如何理解token?

token在项目中的大概流程: 1.客户端使用用户名和密码请求登录 2.服务端收到请求,验证用户名和密码 3.验证成功后,服务端会生成一个token,然后把这个token发送给客户端 4.客户端收到token后把它存储起来,可以放在cookie…

汇编调用C语言定义的全局变量

在threadx移植中,系统的systick通过了宏定义的方式定义,很难对接库函数的时钟频率,不太利于进行维护 所以在C文件中自己定义了一个systick_Div的变量,通过宏定义方式设定systick的时钟频率 在汇编下要加载这个systick分频系数 …

扬州市 自动挡C2 道路驾驶技能考试 电子路要点

先上车把身份证给安全员,验证身份,然后下车逆时针绕车一周,在车头前站立三秒拍照,然后上车 科三基本注意事项 起步不管要不要变道,都必须先打左转向灯,但是也要记得关灯 操作顺序:打左转向灯、…

flutter:BottomNavigationBar和TabBar

区别 BottomNavigationBarr和TabBar都是用于创建导航栏的组件,但它们有一些区别。 位置不同:BottomNavigationBar通常位于屏幕底部,用于主要导航;而TabBar通常位于屏幕顶部或底部,用于切换不同的视图或页面。 样式不…