Creator | 优化三剑客之内存!-程序员宅基地

官方文档:

资源加载:

https://docs.cocos.com/creator/manual/zh/scripting/dynamic-load-resources.html


资源释放:

https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html

34fcdc7c0353e38cd2cf2c03f2f4ceb9.gif

设备对每个程序都有最大的内存分配限制,如果超过了这个阈值,会被系统强制关闭,造成 crash。

因此在开发的过程中,我们要在保证程序运行效率的前提下,尽量压缩程序运行时所占用的内存。

要讨论内存优化,首先要知道项目中最消耗内存的是什么?

就像 Creator 工程中占用空间最多的是资源,资源包括纹理、声音、数据等等


这里我们先了解下 Creator 的资源在内存中的管理方式,再介绍其他的优化内容。

01

存储形式

资源在加载完成后,会以 { uuid : cc.Asset } 的形式被缓存到 cc.assetManager.assets 中,以避免重复加载。

2c733445f8773d396627077550a76a38.png

但是这也会造成内存和显存的持续增长,所以有些资源如果不再需要用到,可以通过 自动释放 或者 手动释放 的方式进行释放。

释放资源将会销毁资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)。

cc.assetManager:管理资源的行为和信息,包括加载,释放等

cc.assetManager.assets :已加载资源的集合

02

引用计数

引用计数 是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。

资源在加载完成后,会返回 cc.Asset 实例, 所有 cc.Asset 实例都拥有成员函数 addRef 和 decRef,分别用于增加和减少引用计数。

初始化引用计数

this._ref = 0;

资源的引用计数 +1

addRef () {
    this._ref++;
    return this;
}

资源的引用计数 -1,并尝试进行自动释放

decRef (autoRelease) {
    this._ref--;
    //接下来会对代码进行详细的解读
    autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
    return this;
}

Asset Manager 只会 自动统计 资源之间的 静态引用,并不能真实地反应资源在游戏中被动态引用的情况,动态引用 还需要 开发者进行控制 以保证资源能够被正确释放。

1静态引用

当开发者在编辑器中编辑资源时(例如场景、预制体、材质等),需要在这些资源的属性中配置一些其他的资源,例如在材质中设置贴图,在场景的 Sprite 组件上设置 SpriteFrame。那么这些引用关系会被记录在资源的序列化数据中,引擎可以通过这些数据分析出依赖资源列表,像这样的引用关系就是 静态引用。

引擎对资源的 静态引用 的统计方式为:

  1. 在 动态加载 某个资源时,引擎会在底层加载管线中记录该资源所有 直接依赖资源 的信息,并将所有 直接依赖资源 的引用计数加 1,然后将该资源的引用计数初始化为 0

  2. 在释放资源时,取得该资源之前记录的所有 直接依赖资源 信息,并将所有依赖资源的引用计数减 1

因为在释放检查时,如果资源的引用计数为 0,才可以被自动释放。所以上述步骤可以保证资源的依赖资源无法先于资源本身被释放,因为依赖资源的引用计数肯定不为 0。也就是说,只要一个资源本身不被释放,其依赖资源就不会被释放,从而保证在复用资源时不会错误地进行释放。

下面我们来看一个例子:

  1. 假设现在有一个 A 预制体,其依赖的资源包括 a 材质和 b 材质。a 材质引用了 α 贴图,b 材质引用了 β 贴图。那么在加载 A 预制体之后,a、b 材质的引用计数都为 1,α、β 贴图的引用计数也都为 1

    2d3dd36f4511a9cac15293e31dfb7ca6.png

  2. 假设现在又有一个 B 预制体,其依赖的资源包括 b 材质和 c 材质。则在加载 B 预制体之后,b 材质的引用计数为 2,因为它同时被 A 和 B 预制体所引用。而 c 材质的引用计数为 1,α、β 贴图的引用计数也仍为 1

    8a23f4b232841ac7f32bdf86df9c3ba7.png

  3. 此时释放 A 预制体,则 a,b 材质的引用计数会各减 1

  • a 材质的引用计数变为 0,被释放,所以贴图 α 的引用计数减 1 变为了 0,也被释放

  • b 材质的引用计数变为 1,被保留,所以贴图 β 的引用计数仍为 1,也被保留

  • 因为 B 预制体没有被释放,所以 c 材质的引用计数仍为 1,被保留

    3e241d50290d67a709320c6bdf5cda91.png

0bd0dbbafc07aaaa31d8692f2fa1aa57.gif

我们通过 creator 来了解下 assets

新建一个场景,不放入任何资源

e5372efe33e1b0bca2df9128590af473.png

打印 assets

console.log(cc.assetManager.assets)

可以看到内存中的资源均为 cocos 的内置资源

42f3dab30c53c6bd952db4fe23c4f2d7.png

在场景中放入 HelloWorld

6f0bae739e58bc5d76745b56c8f261cf.png

启动游戏后,引擎在底层加载管线中调用 assets 的成员方法 addRef

c9264db30531544a70dd1f132d64a873.png

再次打印 assets 及资源的引用计数

console.log(cc.assetManager.assets);
console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);

37f3e97c5cba2e4e56525f2449c87464.png

会发现 assets 多了两项,uuid 分别是

6aa0aa6a-ebee-4155-a088-a687a6aadec4 

31bc895a-c003-4566-a9f3-2e54ae1c17dc

在编辑器中显示 HelloWorld 的 Texture2D 和 SpriteFrame 的 uuid,和上述的两个 uuid 完全匹配

b0aa925e37c91bd6340e59e2d6ef4eaa.png

图片的引用计数也增加为 1

97e6c21b8615872df94c252333b163e0.png

如果存在两份 HelloWorld,但他们的 spriteFrame 是同一份

38df30e28746d7469774880f1fc8e841.png

那么 cc.assetManager.assets 依然保持原样,但 spriteFrame 的 refCount 会变成 2

对于更复杂的资源引用情况,可以自己测试下 assets 及引用计数

补充知识点:Texture 和 SpriteFrame 资源类型

在 资源管理器 中,图像资源的左边会显示一个和文件夹类似的三角图标,点击就可以展开看到它的子资源(sub asset),每个图像资源导入后编辑器会自动在它下面创建同名的 SpriteFrame 资源。

94e54dc5ac676fe3d5441d928b5e4cc6.png

SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像。

为什么会有 SpriteFrame 这种资源?Texture 是保存在 GPU 缓冲中的一张纹理,是原始的图像资源。而 SpriteFrame 包含两部分内容:记录了 Texture 及其相关属性的 Texture2D 对象和纹理的矩形区域,对于相同的 Texture 可以进行不同的纹理矩形区域设置,然后根据 Sprite 的填充类型,如 SIMPLE、SLICED、TILED 等进行不同的顶点数据填充,从而满足 Texture 填充图像精灵的多样化需求。而 SpriteFrame 记录的纹理矩形区域数据又可以在资源的属性检查器中根据需求自由定义,这样的设置让资源的开发更为高效和便利。除了每个文件会产生一个 SpriteFrame 的图像资源(Texture)之外,我们还有包含多个 SpriteFrame 的图集资源(Atlas)类型。

2动态引用

当开发者在编辑器中没有对资源做任何设置,而是通过代码动态加载资源并设置到场景的组件上,则资源的引用关系不会记录在序列化数据中,引擎无法统计到这部分的引用关系,这些引用关系就是 动态引用

使用 动态加载 资源来进行动态引用

  • 动态加载 resources 目录中的资源

8ab27596274f091a66466731b6bfba27.png

cc.resources.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
    this.sprite.spriteFrame = assets;
    console.log(cc.assetManager.assets);
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
});
  • 动态加载 bundle 目录中的资源

a4876473eb8773403332a711b2aab2b2.png

cc.assetManager.loadBundle("bundle", (err: Error, bundle: cc.AssetManager.Bundle) => {
    bundle.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
        this.sprite.spriteFrame = assets;
        console.log(cc.assetManager.assets);
        console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
    });
});

在资源加载完成后打印下 assets 及资源的引用计数

a1bfa1a641b09a27522dfb46c8708d18.png

可以看到,资源加载完成后会将 SpriteFrame 资源设置到 Sprite 组件上,但引擎不会做特殊处理,SpriteFrame 的引用计数仍保持 0,此时需要我们手动来管理引用计数。

增加引用计数

cc.resources.load("HelloWorld", cc.SpriteFrame, (err, assets: cc.SpriteFrame) => {
    this.sprite.spriteFrame = assets;
    this.sprite.spriteFrame.addRef();
    console.log(cc.assetManager.assets);
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
});

减少引用计数(为了避免过多的资源干扰视线,我们在触摸结束时减少引用计数)

onTouchEnd(event: cc.Event.EventTouch) {
    console.log("###");


    this.sprite.node.destroy();


    this.sprite.spriteFrame.decRef();
    console.log("spriteFrame.refCount : " + this.sprite.spriteFrame.refCount);
    this.sprite.spriteFrame = null;
    //在下一帧打印 assets
    this.scheduleOnce(()=>{
        console.log(cc.assetManager.assets);
    });
}

运行后的 log

74a73bab6b54f7bbaa392bcf3c650173.png

从 log 中可以看到,addRef 后,资源的引用计数变为 1,decRef 之后资源的引用计数在当前帧为 0,在下一帧,资源也从 assets 中被清除了。

注意:

动态加载 的资源必须手动卸载,卸载方式

① 通过引用计数:addRef  和 decRef

② 直接释放:releaseAsset

在资源加载完成后,会被临时缓存到 cc.assetManager.assets 中,以便下次复用。但是这也会造成内存和显存的持续增长,所以有些资源如果不需要用到,可以通过 自动释放 或者 手动释放 的方式进行释放。释放资源将会销毁资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)

3自动释放

① 场景自动释放

在 资源管理器 选中场景后,属性检查器 中会出现 自动释放资源 选项。

1258b2cf25427fd6f6f5ff021694b514.png

勾选后,点击右上方的 应用 按钮,之后在切换该场景时便会自动释放该场景所有 静态引用 的依赖资源。建议场景尽量都勾选自动释放选项,以确保内存占用较低,除了部分高频使用的场景(例如主场景)。

② 资源自动释放

所有 cc.Asset 实例都拥有成员函数 addRef 和 decRef,分别用于增加和减少引用计数。一旦引用计数为零,Creator 会对资源进行自动释放(需要先通过释放检查,具体可参考下部分内容的介绍)。

start () {
    cc.resources.load('images/background', cc.Texture2D, (err, texture) => {
        this.texture = texture;
        // 当需要使用资源时,增加其引用
        texture.addRef();


    });
}


onDestroy () {
    // 当不需要使用资源时,减少引用
    // Creator 会在调用 decRef 后尝试对其进行自动释放
    this.texture.decRef();
}

自动释放的优势在于不用显式地调用释放接口,开发者只需要维护好资源的引用计数,Creator 会根据引用计数自动进行释放。这大大降低了错误释放资源的可能性,并且开发者不需要了解资源之间复杂的引用关系。对于没有特殊需求的项目,建议尽量使用自动释放的方式来释放资源。

4手动释放

当项目中使用了更复杂的资源释放机制时,可以调用 Asset Manager 的相关接口来手动释放资源。

cc.assetManager.releaseAsset(texture);

说明

  1. cc.assetManager.releaseAsset 接口仅能释放单个资源,且为了统一,接口只能通过资源本身来释放资源,不能通过资源 uuid、资源 url 等属性进行释放

  2. 在释放资源时,开发者只需要关注资源本身,引擎会 自动释放 其依赖资源(getDeps)

注意:

release 系列接口(例如 release、releaseAsset、releaseAll)会直接释放资源,而不会进行释放检查,只有其依赖资源会进行释放检查。所以当显式调用 release 系列接口时,可以确保资源本身一定会被释放。

5释放检查

为了避免错误释放正在使用的资源造成渲染或其他问题,Creator 会在自动释放资源之前进行一系列的检查,只有检查通过了,才会进行自动释放。

  1. 如果资源的引用计数为 0,即没有其他地方引用到该资源,则无需做后续检查,直接摧毁该资源,移除缓存

  2. 资源一旦被移除,会同步触发其依赖资源的释放检查,将移除缓存后的资源的 直接 依赖资源(不包含后代)的引用都减 1,并同步触发释放检查

  3. 如果资源的引用计数不为 0,即存在其他地方引用到该资源,此时需要进行循环引用检查,避免出现自己的后代引用自己的情况。如果循环引用检查完成之后引用计数仍不为 0,则终止释放,否则直接摧毁该资源,移除缓存,并触发其依赖资源的释放检查(同步骤 2)

a53582f60292e1d82b1799ff153e6d24.gif

我们通过 creator 来了解下资源释放的过程

新建一个场景,不放入任何资源

bbc22c66c27f9c57e757b60c0ff6de35.png

打印 assets

console.log(cc.assetManager.assets)

可以看到内存中的资源均为 cocos 的内置资源

012acf380ce6029a948725111958bd68.png

在场景中放入 HelloWorld

2227e14a1027573844dd97139fb06566.png

为了避免过多的资源干扰视线,我们在触摸结束时 手动释放 该节点的图片资源

onTouchEnd(event: cc.Event.EventTouch) {
    cc.assetManager.releaseAsset(this.sprite.spriteFrame);
    console.log(cc.assetManager.assets);
}

再次打印 assets,可以看到释放该资源后,assets 又回到了初始状态

6fbcd5fc5b154ec96b169d4a581d29d9.png

那么 releaseAsset 究竟做了什么?

查阅 assets 相关的源码

52c368f7254cfa9e2fbc84764034a52e.png

断点+单步调试,可以快速的理清脉络

8ad1a5a9d314629abb01b6947c901939.png

db4c771104a9399453a79e0a0115c92a.png

整理后的大致流程:

9109d9cdf9eeafc19791bd00c0dcf5f6.png

下面是对源码的一些注释,配合流程图服用,效果更佳

手动释放

releaseAsset (asset) {
    //强制释放
    releaseManager.tryRelease(asset, true);
}

减少资源的引用并尝试进行自动释放

decRef (autoRelease) {
    this._ref--;
    autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
    return this;
}

尝试进行释放

tryRelease (asset, force) {
    if (!(asset instanceof cc.Asset)) return;
    if (force) {
        //强制释放
        releaseManager._free(asset, force);
    }
    else {
        //非强制释放则添加到待删除队列
        _toDelete.add(asset._uuid, asset);
        if (!eventListener) {
            //已监听渲染过程之后所触发的事件
            eventListener = true;
            //渲染过程之后执行释放
            cc.director.once(cc.Director.EVENT_AFTER_DRAW, freeAssets);
        }
    }
}

尝试自动去释放依赖资源并释放该资源

_free (asset, force) {
    //从待删除队列中移除
    _toDelete.remove(asset._uuid);


    if (!cc.isValid(asset, true)) return;


    if (!force) {
        //非强制释放则判断引用计数
        if (asset.refCount > 0) {
            //检查该资源的循环引用,返回其引用计数
            if (checkCircularReference(asset) > 0) return; 
        }
    }


    // remove from cache
    assets.remove(asset._uuid);
    //获取资源直接引用的非原生依赖列表,例如,材质的非原生依赖是 Texture
    var depends = dependUtil.getDeps(asset._uuid);
    for (let i = 0, l = depends.length; i < l; i++) {
        var dependAsset = assets.get(depends[i]);
        if (dependAsset) {
            //减少资源的引用计数
            dependAsset.decRef(false);
            releaseManager._free(dependAsset, false);
        }
    }
    asset.destroy();
    dependUtil.remove(asset._uuid);
}

释放待删除队列中的资源

function freeAssets () {
    eventListener = false;
    _toDelete.forEach(function (asset) {
        releaseManager._free(asset);
    });
    _toDelete.clear();
}

ada8ba56fd7efa682cfff0549d89cf06.gif

最后一个值得关注的要点:JavaScript 的垃圾回收是延迟的

在 C 与 C++ 等语言中,开发人员可以直接控制内存的申请和回收,而 JavaScript 所有对象的内存都由垃圾回收机制来管理,会周期性对那些我们不再使用的变量、对象所占用的内存进行释放,这就导致 JS 层逻辑永远不知道一个对象会在什么时候被释放。

想象一种情况,当你释放了 AssetManager 对某个资源的引用之后,由于考虑不周的原因,游戏逻辑再次请求了这个资源,这时垃圾回收还没有开始(垃圾回收的时机不可控)。

当出现这个情况时,意味着这个资源还存在内存中,但是 AssetManager 已经访问不到了,所以会重新加载它,就会造成这个资源在内存中有两份同样的拷贝,一份为刚刚请求的,另一份为已经释放但未被回收的,形成资源在内存中 暂时性 的 冗余。

之所以说暂时性,是因为在下个 GC 周期时,该资源依然会被回收,释放对应的内存。

如果只是一个资源还好,但是如果类似的资源很多,甚至不止一次被重复加载,就会造成当前时间内存飙升,而且频繁GC也会影响游戏的流畅性

因此我们释放资源时,应该 避免频繁释放,同时 避免释放近期内将要复用的资源。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44053279/article/details/129573170

智能推荐

MyBatis-Plus 2.x版本 乐观锁采坑记_mybatis mybatisplus 2.x 最稳定版本-程序员宅基地

文章浏览阅读1k次。主要适用场景当要更新一条记录的时候,希望这条记录没有被别人更新乐观锁实现方式:取出记录时,获取当前version更新时,带上这个version执行更新时, set version = yourVersion+1 where version = yourVersion如果version不对,就更新失败乐观锁配置需要2步 记得两步1、插件配置spring xml<bean class="com.baomidou.mybatisplus.plugins.OptimisticLocke_mybatis mybatisplus 2.x 最稳定版本

使用python-docx实现对word文档里的字符串、图片批量替换_def replace_word(doc, tag, pv): # replace in parag-程序员宅基地

文章浏览阅读5.2k次,点赞9次,收藏55次。python-docx 是用于创建和更新Microsoft Word(.docx)文件的Python库。它的API文档也非常简单,看完之后,只能简单理解一些基础用法。最近碰到一个需求,需要对word模版里的内容进行统一替换,替换内容比较多。从网上查到了很多种基于python-docx 的做法,但都有一定的缺陷,不能适用于各种场景。网上的做法整体是两种:相对比较好的解决办法:对runs中的内容进行一定程度的拼接,但是有缺点,部分文字的样式可能会消失,可以尽量让每一段文字的样式保持一致来避免这种情况。..._def replace_word(doc, tag, pv): # replace in paragraph

高斯混合模型在无监督学习中的表现-程序员宅基地

文章浏览阅读754次,点赞24次,收藏20次。1.背景介绍无监督学习是一种通过分析数据中的模式和结构来自动发现隐含结构的学习方法。它主要应用于数据竞争中,通过对数据的分类、聚类、降维等方式来提取数据中的知识。高斯混合模型(Gaussian Mixture Model, GMM)是一种常用的无监督学习方法,它假设数据是由多个高斯分布组成的混合分布,并通过估计这些高斯分布的参数来实现数据的聚类。在本文中,我们将详细介绍高斯混合模型在无监督...

华云大咖说 | 华云数据助力企业桌面快速上云_华云数据 体验最好的云桌面-程序员宅基地

文章浏览阅读173次。近年来,国家把科技自立自强作为国家发展的战略支撑,越来越多的行业企业将自主创新、自主研发作为发展的重中之重,以此增强企业的核心竞争实力。在今年发布的政府工作报告中,也对推动科技创新发展提出了众多战略要求。华云数据是中国云计算独角兽,自成立以来,始终坚持以技术创新为根本,坚持自主研发,至今已获得500余项知识产权。已经具备支持多芯多栈的解决方案,在复杂的混合IT架构背景下,可以为用户提供完整的数据中心云化、云上办公、信创转型和公有云方案。随着5G、云计算、大数据等技术的快速发展,以及移动办公、远程协作等._华云数据 体验最好的云桌面

解决coursera课程国内打不开的问题_coursera在中国能看吗-程序员宅基地

文章浏览阅读564次。2021.8.8亲测有效https://zhuanlan.zhihu.com/p/77631369_coursera在中国能看吗

JOJ 1055: Cog-Wheels 解题报告 _cog-wheels解题思路-程序员宅基地

文章浏览阅读744次。这个题太恶心,一开始给想错了。光把分子分母看能否分别表示是不对的一组反例 8 3 14 表示 7:12 这时就需要加倍数,14:24却是可以分开表示的。 代码写的不好,有几个地方可以改动的 老规剧 题意: 给定一些数字 ,运用这些数字作乘除运算,看能不能得到指定比例。 分析 : 1. 把所给的数字能得到的最_cog-wheels解题思路

随便推点

python官方文档中文版下载,python官方手册中文pdf-程序员宅基地

文章浏览阅读3k次。Python参考手册(第4版)》(David M.Beazley)电子书网盘下载免费在线阅读资源链接:链接: 提取码: kybr 书名:Python参考手册(第4版)作者:David M.Beazley译者:谢俊豆瓣评分:7.4出版社:人民邮电出版社出版年份:2010-12页数:540内容简介:本书是权威的Python语言参考指南,内容涉及核心Python语言和Python库的最重要部分。因此,它作为一个两卷本的合集中的第一本:《Learning Python》,也就是这本书,介绍Python本身。_python官方文档中文版下载

java ee核心技术与应用_Java EE核心技术与应用(全面覆盖Java EE 6) 郝玉龙等著 pdf扫描版[103MB]...-程序员宅基地

文章浏览阅读92次。Java EE 核心技术与应用基于最新的Java EE 6规范对Java EE应用开发技术进行系统讲解。书中主要包括四部分内容:第一部分介绍了Java EE的定义、设计思想、技术架构和开发模式等,可使读者全面认识Java EE。第二部分以Java EE企业应用的表现层、数据持久化层和业务逻辑层的开发为主线,重点讲解Java EE 6 规范的最新功能特性,包括JSF 2.0、Servlet3.0、E..._java ee高级技术及应用

facebook的开源梯度优化工具Nevergrad_nevergrad opt-程序员宅基地

文章浏览阅读1.1k次。facebook的开源梯度优化工具github:https://github.com/facebookresearch/nevergrad官方原文地址:https://code.fb.com/ai-research/nevergrad/Nevergrad: An open source tool for derivative-free optimizationMost machin..._nevergrad opt

antd Vue项目 日期组件限制年月周日_a-month-picker-程序员宅基地

文章浏览阅读1.3k次。【代码】antd Vue项目 日期组件限制年月周日。_a-month-picker

ORBSLAM3 的改进_orb改进-程序员宅基地

文章浏览阅读9.6k次,点赞17次,收藏114次。周六看到了ORBSLAM3的源码,安装运行后看了一下其代码结构,因为加IMU的部分是针对之前的ORB-VI, 因此大家可以参考jinpang的LearnORBVI可以更纯粹地学习视觉+IMU的组合;这篇文章主要是针对其在Tracking线程做出的改动,尤其是添加Atlas后对Tracking部分的影响,LoopClosing和MapMerging的部分会在后面的分析中讲到,有错误也欢迎各位指正。ORBSLAM3相对于ORBSLAM2做出的主要改动:1. Atlas: 用于保存很多琐碎的地图;主要_orb改进

livechart 只显示 y 值_图片显示特斯拉上海超级工厂 Model Y 生产线已基本准备就绪 - 特斯拉...-程序员宅基地

文章浏览阅读49次。10 月 23 日消息,据国外媒体报道,在一期的 Model 3 生产线建成投产之后,特斯拉上海超级工厂今年开始了二期的大规模建设,主要是建设 Model Y 的生产厂房,所生产的 Model Y 计划在明年开始交付。从特斯拉最新公布的图片来看,在经过大半年的建设之后,上海超级工厂 Model Y 生产厂房的外部施工已基本结束,内部的生产线也已基本准备就绪。特斯拉是在新近发布的三季度财报中,公布上..._特斯拉工厂python

推荐文章

热门文章

相关标签