【iOS】——AutoReleasePool底层原理及总结

news2024/11/26 5:50:51

自动释放池

AutoreleasePool自动释放池用来延迟对象的释放时机,将对象加入到自动释放池后这个对象不会立即释放,等到自动释放池被销毁后才将里边的对象释放。

自动释放池的生命周期

  1. 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
  2. 用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
  3. runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
  4. 在一次完整的runloop结束之前,会向自动释放池中所有对象发送release消息,然后销毁自动释放池

AutorealeasePool结构

每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节 。

AutorealeasePool就是由AutoreleasePoolPage构成的双向链表,AutoreleasePoolPage是双向链表的节点

在这里插入图片描述

AutoreleasePoolPage的定义如下:

class AutoreleasePoolPage 
{
    //magic用来校验AutoreleasePoolPage的结构是否完整
    magic_t const magic;                   // 16字节
    //指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();
    id *next;                              // 8字节
    //thread指向当前线程
    pthread_t const thread;                // 8字节
    //parent指向父节点,第一个节点的parent指向nil;
    AutoreleasePoolPage * const parent;    // 8字节 
    //child 指向子节点,第一个节点的child指向nil;
    AutoreleasePoolPage *child;            // 8字节
    //depth 代表深度,从0开始往后递增1;
    uint32_t const depth;                  // 4字节
    //hiwat 代表high water mark;
    uint32_t hiwat;                        // 4字节
    ...
}

自动释放池中的栈

如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:

在这里插入图片描述

其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象

begin()end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。

next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中

在这里插入图片描述

POOL_SENTINEL(哨兵对象)

POOL_SENTINEL 就是哨兵对象,它是一个宏,值为nil,标志着一个自动释放池的边界。

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL

在这里插入图片描述

AutoreleasePool实现

在每个文件中的main.m文件中都有下面这段代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在整个main函数中只有一个autoreleasepool块,在块中只包含了一行代码,这行代码将所有的事件、消息全部交给了 UIApplication 来处理,也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的

将main.m文件通过$ clang -rewrite-objc main.m重新编译生成发现aotuoreleasepool 被转换为为一个 __AtAutoreleasePool 结构体:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。

所以实际上 main 函数在实际工作时其实是这样的:

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();

        // do whatever you want

        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

所以autoreleasepool的实现主要靠objc_autoreleasePoolPush()objc_autoreleasePoolPop() 来实现

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

而在objc_autoreleasePoolPush()objc_autoreleasePoolPop() 中又分别调用了AutoreleasePoolPage类的push和pop方法。

入栈

在这里插入图片描述

objc_autoreleasePoolPush()

首先调用objc_autoreleasePoolPush()

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
AutoreleasePoolPage::push()
static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

该函数就是调用了关键的方法 autoreleaseFast,并传入哨兵对象POOL_SENTINEL

autoreleaseFast()

static inline id *autoreleaseFast(id obj)
    {
        //1. 获取当前操作页
        AutoreleasePoolPage *page = hotPage();
        //2. 判断当前操作页是否满了
        if (page && !page->full()) {
            //如果未满,则压桟
            return page->add(obj);
        } else if (page) {
            //如果满了,则安排新的页面
            return autoreleaseFullPage(obj, page);
        } else {//页面不存在,则新建页面
            return autoreleaseNoPage(obj);
        }
    }

hotPage 可以为当前正在使用的 AutoreleasePoolPage

上面代码主要分为三种情况:

  • hotPage 并且当前 page 不满

调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

  • hotPage 并且当前 page 已满

调用 autoreleaseFullPage 初始化一个新的页接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

  • 无hotPage

调用 autoreleaseNoPage 创建一个 hotPage,接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

总的来说这三种情况最后都会调用 page->add(obj) 将对象添加到自动释放池中。

page->add 添加对象
//入桟对象
id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        //传入对象存储的位置(比' return next-1 '更快,因为有别名)
        id *ret = next; 
        //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置
        *next++ = obj;
        protect();
        return ret;
    }

这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针

autoreleaseFullPage(当前 hotPage 已满)
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage

如果找到最后还是没找到创建一个新的 AutoreleasePoolPage

将找到的或者构建的page标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。

autoreleaseNoPage(没有 hotPage)
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

创建一个新的page,并且将新的page设置为hotpage。接着调用page->add 方法添加POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。最后,将 obj 添加到autoreleasepool中

既然当前内存中不存在 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。

出栈

在这里插入图片描述

objc_autoreleasePoolPop

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

在这个方法中传入不是哨兵对象而是传入其它的指针也是可行的,会将自动释放池释放到相应的位置。

AutoreleasePoolPage::pop

template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        //出桟当前操作页面对象
        page->releaseUntil(stop);

        // 删除空子项
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            //特殊情况:在每个页面池调试期间删除所有内容
            //获取当前页面
            AutoreleasePoolPage *parent = page->parent;
            //将当前页面杀掉
            page->kill();
            //设置父节点页面为当前操作页
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            //特殊情况:当调试丢失的自动释放池时,删除所有pop(top)
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
            //滞后:如果页面超过一半是满的,则保留一个空的子节点
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

该静态方法总共做了三件事情:

  1. 使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
  2. 调用 releaseUntil 方法释放栈中的对象,直到 stop
  3. 调用 childkill 方法
  1. pop之后,所有child page肯定都为空了,且当前page一定是hotPage
  2. 系统为了节约内存,判断,如果当前page空间使用少于一半,就释放掉所有的child page,如果当前page空间使用大于一半,就从孙子page开始释放,预留一个child page
pageForPointer 获取 AutoreleasePoolPage

pageForPointer 方法主要是通过内存地址的操作,获取当前指针所在页的首地址:

static AutoreleasePoolPage *pageForPointer(const void *p) {
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    assert(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:

p = 0x100816048
p % SIZE = 0x48
result = 0x100816000

而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage

通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1

releaseUntil 释放对象
void releaseUntil(id *stop) 
{
    // 这里没有使用递归, 防止发生栈溢出
    while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage(); // 取出 hotPage

        while (page->empty()) { // 从节点 page 开始, 向前找到第一个非空节点
            page = page->parent; // page 非空的话, 就向 page 的 parent 节点查找
            setHotPage(page); // 把新的 page 节点设置为 HotPage
        }

        page->unprotect(); // 如果需要的话, 解除 page 的内存锁定
        id obj = *--page->next; // 先将 next 指针向前移位, 然后再取出移位后地址中的值
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将 next 指向的内存清空为SCRIBBLE
        page->protect(); // 如果需要的话, 设置内存锁定

        if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符
            objc_release(obj); // 给取出来的对象进行一次 release 操作
        }
    }

    setHotPage(this); // 将本节点设置为 hotPage

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

调用者是用 pageForPointer() 找到的, token 所在的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历存储在节点里的 autorelease 对象列表, 对每个对象进行一次 release 操作, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 一直到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 所以这个时候的 hotPage 应该是 this.

kill() 方法
void kill() 
{
    // 这里没有使用递归, 防止发生栈溢出
    AutoreleasePoolPage *page = this; // 从调用者开始
    while (page->child) page = page->child; // 先找到最后一个节点

    AutoreleasePoolPage *deathptr;
    do { // 从最后一个节点开始遍历到调用节点
        deathptr = page; // 保留当前遍历到的节点
        page = page->parent; // 向前遍历
        if (page) { // 如果有值
            page->unprotect(); // 如果需要的话, 解除内存锁定
            page->child = nil; // child 置空
            page->protect(); // 如果需要的话, 设置内存锁定
        }
        delete deathptr; // 回收刚刚保留的节点, 重载 delete, 内部调用 free
    } while (deathptr != this);
}

自动释放池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都回收, 操作方案就是从最后一个节点, 遍历到调用者节点, 挨个回收

总结

  • 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,每一个AutoreleasePoolPage所占内存大小为4096字节,其中56字节用于存储结构体中的成员变量。
  • autoreleasepool在初始化时,内部是调用objc_autoreleasePoolPush方法
  • autoreleasepool在调用析构函数释放时,内部是调用objc_autoreleasePoolPop方法

入栈(push)

在页中压栈普通对象主要是通过next指针递增进行的

  • 当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
  • 当页未满,将autorelease对象插入到栈顶next指针指向的位置(向一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置)
  • 当页满了(next指针马上指向栈顶),建立下一页page对象,设置页的child对象为新建页,新page的next指针被初始化在栈底(begin的位置),下次可以继续向栈顶添加新对象。

在这里插入图片描述

出桟(pop)

在页中出栈普通对象主要是通过next指针递减进行的

  • 根据传入的哨兵对象地址找到哨兵对象所处的page
  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置.(从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理))
  • 当页空了时,需要赋值页的parent对象为当前页

image.png

Autorelease对象什么时候释放

当创建了局部释放池时,会在@autoreleasepool{}的右大括号结束时释放,及时释放对象大幅度降低程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的(每个线程对应一个runloop),而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池。

AutoreleasePool能否嵌套使用

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

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

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中 设置为autorelease的对象不需要手动释放,会直接进入自动释放池 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中

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

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视一个事件:

监听 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:

BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;

Exit(即 将退出 Loop) 时调用_objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

thread 和 AutoreleasePool的关系

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

RunLoop 和 AutoreleasePool的关系

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

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

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

相关文章

C++初学(11)

不知不觉就第11篇了QWQ 11.1、指针和自由存储空间 之前提到了计算机程序在存储数据时必须跟踪的3个基本属性&#xff1a; &#xff08;1&#xff09;信息存储在何处&#xff1b; &#xff08;2&#xff09;存储的值为多少&#xff1b; &#xff08;3&#xff09;存储的信息…

GitHub爆赞的Web安全防护指南,网络安全零基础入门必备教程!

web安全现在占据了企业信息安全的很大一部分比重&#xff0c;每个企业都有对外发布的很多业务系统&#xff0c;如何保障web业务安全也是一项信息安全的重要内容。 然而Web 安全是一个实践性很强的领域&#xff0c;需要通过大量的练习来建立对漏洞的直观认识&#xff0c;并积累…

xtrabackup搭建MySQL 8.0 主从复制

xtrabackup搭建MySQL 8.0 主从复制 安装MySQL 8.0.37安装xtrabackupGTIDs初始化从库参考&#xff1a;GTID概述GTID相较与传统复制的优势GTID自身存在哪些限制GTID工作原理简单介绍如何开启GTID复制GTID与传统模式建立复制时候语句的不同点传统复制GTID复制 GTID同步状态简单解析…

Docker技术背景与应用:解决现代开发中的关键问题

目录 Docker技术背景与应用&#xff1a;解决现代开发中的关键问题 一、Docker的技术背景 1. 什么是Docker&#xff1f; 2. Docker的核心组件 3. Docker的历史发展 二、Docker解决了哪些问题&#xff1f; 1. 环境一致性问题 2. 依赖管理问题 3. 部署复杂性问题 4. 资源…

微信小程序实现上传照片功能

案例&#xff1a; html: <view class"zhengjianCont fontSize30" style"margin-bottom: 40rpx;"><view class"kuai"><image binderror"imageOnloadError" bind:tap"upladPhoto" data-params"business…

二叉树——2.对称二叉树

力扣题目链接 给定一个二叉树&#xff0c;检查它是否是镜像对称的。 示例&#xff1a; 上述的二叉树就是对称的。 在做二叉树题目时&#xff0c;最重要的是你要找到题目中的二叉树是怎么遍历的。本题中要检查二叉树是否镜像对称&#xff0c;这就不是单独判断某个父节点的左右…

Java二十三种设计模式-策略模式(13/23)

策略模式&#xff1a;灵活算法的替换与扩展 引言 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了算法族&#xff0c;分别封装起来&#xff0c;让它们之间可以互相替换&#xff0c;此模式让算法的变化独立于使用算法的客户。 基础…

menuconfig+Kconfig的简单配置

目录 1.背景 2.管理方案 2.1&#xff1a;.h中直接定义 2.2&#xff1a;.batCmake 2.3&#xff1a;Kconfig 2.3.1 环境安装 2.3.2 代码 2.3.2.1 目录结构 2.3.2.2 ble目录下的Kconfig 2.3.2.3 hardware目录下的Kconfig 2.3.2.4 rtos目录下的Kconfig 2.3.2.5 根目录 …

【性能】console.log引起内存泄漏

如下代码中的console.log会引起内存泄漏 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Example<…

降级、熔断、限流学习笔记

1. 面对高流量出现故障的原因 由于依赖的资源或者服务不可用&#xff0c;最终导致整体服务宕机。在电商系统中就可能由于数据库访问缓慢&#xff0c;导致整体服务不可用。 乐观地预估了可能到来的流量&#xff0c;当有超过系统承载能力的流量到来时&#xff0c;系统不堪重负&a…

Vue.js 3.x 必修课|008|计算属性:提高代码服用性和可维护性

欢迎关注公众号:CodeFit。 创作不易,如果你觉得这篇文章对您有帮助,请不要忘了 点赞、分享 和 关注,为我的 持续创作 提供 动力! 欢迎订阅《Vue 3.x 必修课| 2024》:http://t.csdnimg.cn/hHRrM 精品内容,物超所值,一杯咖啡的价格(9.9 元)只为持续创作提供动力。 在 …

【AI】人工智能时代,程序员如何保持核心竞争力?

目录 程序员在AI时代的应对策略1. 引言2. AI在编程领域的影响2.1 AI辅助编程工具的现状2.2 AI对编程工作的影响2.3 程序员的机遇与挑战 3. 深耕细作&#xff1a;专注领域的深度学习3.1 专注领域的重要性3.2 深度学习的策略3.2.1 选择合适的领域3.2.2 持续学习和研究3.2.3 实践与…

【PXE+kickstart】linux网络服务之自动装机

PXE&#xff1a; 简介&#xff1a;PXE(Preboot execute environment 是一种能够让计算机通过网络启动的引导方式&#xff0c;只要网卡支持PXE协议即可使用Kickstart 是一种无人值守的安装方式&#xff0c;工作原理就是预先把原本需要运维人员手工填写的参数保存成一个 ks.cfg 文…

centos7安装 ES集群 elasticsearch

这里写自定义目录标题 编写启动脚本 elasticsearch.sh启动可能报错&#xff1a;elasticsearch 7.10启动报错 bootstrap checks failed解决方法问题原因&#xff1a;注意 退出xshell&#xff0c;重新登录&#xff1a; 上面两个配置项改完后&#xff0c;ES启动用户(es 或root) **…

Debian | 更换 Gnome 至 Xfce4

Debian | 更换 Gnome 至 Xfce4 更新源 sudo apt update && sudo apt upgrade安装 xfce4 sudo apt install xfce4我选择 lightdm&#xff0c;回车 切换桌面 sudo update-alternatives --config x-session-manager输入 xfce 所在序号&#xff0c;我这里是 3 卸载 …

洛谷 P1560 [USACO5.2]蜗牛的旅行Snail Trails(c++)

describe 蜗牛在制定今天的旅游计划&#xff0c;有 n 个景点可选&#xff0c;它已经把这些景点按照顺路游览的顺序排 成一排了&#xff0c;每个地方有相应的景观&#xff0c;这里用一个整数表示。 蜗牛希望选取连续的一段景点&#xff0c;还要选出来的每一个景点的景观都不同…

ASP.NET Core基础 - 简介

目录 一. 简介 A、跨平台性 B、高性能 C、开源性 D、模块化与可扩展性 E、集成现代前端技术 二. ASP.NET 4.x 和 ASP.NET Core 比较 A、架构与平台支持 B、性能 C、开发体验 D、社区支持与生态系统 三. NET 与 .NET Framework 比较 A、概念范围 B、跨平台能力 C…

文献综述如何有助于识别研究中的关键变量和概念

VersaBot文献综述助手 进行良好的文献综述对于从多个方面确定研究的关键变量和概念起着至关重要的作用&#xff1b; 1.揭示相关领域和理论&#xff1a; 通过沉浸在现有的学术研究中&#xff0c;你会遇到围绕你的主题的各种理论和概念。这些可以作为识别与您的研究问题相关的潜…

天和环保业绩波动性明显,应收账款逾期率和回款率欠佳

《港湾商业观察》施子夫 7月17日&#xff0c;北交所网站更新唐山天和环保科技股份有限公司&#xff08;以下简称&#xff0c;天和环保&#xff09;及保荐机构江海证券关于第三轮审核问询函的回复。 公开信息显示&#xff0c;2023年6月&#xff0c;天和环保的IPO申请获受理。今…

Linux IPC解析:匿名命名管道与共享内存

目录 一.IPC机制介绍二.匿名与命名管道1.匿名管道2.命名管道3.日志 三.共享内存三.System V 标准1.System V简介2.IPC在内核的数据结构设计3.信号量 一.IPC机制介绍 IPC&#xff08;Inter-Process Communication&#xff0c;进程间通信&#xff09;是计算机系统中不同进程之间交…