CutTheKnob 前言 通过前面的文章,我们讨论了打包,下载,解压,此时来到了最后也是最关键的一步,就是如何加载我们的AB包以及资源,本文就为了你阐述资源加载的流程 状态 完结撒花🎉 资源加载 1. 获取AB包的路径 按照之前的命名规则,我们可以获取到模块的热更的配置文件路径或者内嵌资源的文件路径,如上图 2. 通过配置文件加载AB包信息到字典中 在前一步,我们会判断模块的配置资源路径是否存在,如果存在的话,我们就会将其反序列为BundleConfig对象,并且添加到字典中,键为AB包的crc值,而值为BundleItem,其存放AB包的相关信息 (关于crc,这是一种加密算法,我之后可能会研究下其原理,这里给一个链接大家可以大致看下,不看也没关系,当做是唯一值用于作为资源的唯一标识就可以了 https://blog.csdn.net/weixin_42155726/article/details/90040017) 注意在加入完字典后,把AB包释放掉,我们之后在游戏中需要加载资源的时候再加载回来。 同时,用一个List把加载过的模块保存起来,这样之后就可以判断是否重复加载过该模块 3. 加载AB包 上一步,我们把配置存到字典之中,然后,我们就可以进行AB包的加载了,首先,加载的时候,我们要通过crc来获取 其对应的BundleItem对象,接着,我们对其进行判空,如果AB包没有加载进内存,则我们需要将其加载到内存之中。 我们会从引用池子中取出一个AB包的缓存对象,接着,我们会进行是否为热更的判断,得到AB包的路径,然后通过AB包本身的API -> LoadFromFile(路径) 来加载AB包,要注意的是,如果加密了,我们需要先解密加载到内存中,然后通过LoadFromMemory(路径)这个API来加载AB包。 加载完成后,我们会对引用计数进行增加,并且添加到负责记录哪些AB包被加载了的字典中,用于之后判断和检索使用。 有的同学可能对引用计数和引用池不在了解,可以看下下面的内容,如果了解的同学,可以直接跳过。 引用计数 关于引用计数的原理和解释在这里 ZMAssetsFrame学习笔记(1) 引用池 引用池子的本质就是一个栈,我们会在其初始化的时候,new一定数量的对象,而后在使用时,我们会先拿这些已经new了的对象来使用,当不用的时候,再丢回池子之中,这样可以复用对象,避免频繁new对象。 4. 同步加载资源 有了加载AB包的方法后,我们只要传入要加载的资源的路径,就可以得到其相应的BundleItem对象信息,从而得知其在哪个AB包中,然后加载AB包,再使用AB包的LoadAsset<T>()泛型方法,加载资源 并且将其加入到存储资源信息的字典中,用于之后的判断和检索使用 5.异步加载资源 如上图,异步加载资源其实跟同步差不多,唯一区别是我们添加了一个参数作为回调函数,并且如果item.obj不为空,说明该资源被加载过了,直接执行回调 如上图,我们在加载AB包的时候,会使用AB对象本身的LoadAssetAsync<T>()泛型接口对AB包进行异步加载,而后就是把回调函数添加到异步对象request的completed事件中,这样在AB包加载完成后,就会执行lambda表达式中的逻辑,执行回调。 6. 卸载AB包和资源 有了加载AB包,加载资源的功能之后,我们就要面临卸载AB包,释放资源的需求。首先我们先来看看如何卸载AB包。 释放AB包,我们有两个来源,一个是BundleItem中的包名,这个一般是当前包传进来的,第二个来源,则是这个当前包的依赖项传进来的。为什么会有后一个来源呢? 因为我们设想,A包依赖B包和C包。没有加载任何包时,ABC的引用计数分别为A = 0,B = 0,C = 0。 接着,我们加载A包,此时 A = 1, B = 1, C = 1。 加载完后,我们尝试卸载A包,那么此时A包就是当前包,显然A包就会先把自己的引用计数减去,因而A = 0。 但同时,我们A包依赖于B包和C包,因此,我们需要获取A包的所有依赖列表,遍历,然后减去这些AB包的引用计数,在此处,就是B包和C包,其代码,如下图红框所示 由于第一个参数为null,我们就用的是第三个参数,因而就是尝试卸载B包和C包,使得B = 0,C = 0。 然后当 bundleCacheItem.referenceCount <= 0时,说明某个AB包缓存再也不被任何资源或者AB包缓存引用了,此时我们就可以将其记录的AB包卸载,从AB包加载记录字典中移除,然后把这个AB包缓存对象放回池子中。 至此,我们就完成了AB包的卸载,而卸载资源,其实就是将其BundleItem传入,然后执行相关的卸载函数如下图。 至此,A包,B包,C包都会从内存中被卸载掉。 7. 同步克隆物体 因为在Unity开发中,我们常常会加载.prefab文件来实例化GameObject对象,因此会对其做一个接口用于克隆物体。我们先从同步做起。 如上图,我们首先对从对象池中查询对象,如果存在就直接使用,不存在的话,我们就用加载资源的接口,获取其Prefab,接着我们再调用Instantiate()接口进行实例化。 实例化的同时,我们会从引用池中获取一个CacheObject对象来存放对象的相关信息,并且将其instanceID作为键存放到记录所有CacheObject对象的字典中。 这里可能有同学就疑惑,为什么上面还有一个对象池呢?其实,当我们用完这个GameObject对象后,我们会释放它,然后看看它有没有建立对象池,如果没有的话,我们会为其创建对象池。而同一个crc值的GameObject才会放入同一个对象池子中,因而不同对象池的创建依据是crc值。你将会在后面讲销毁的时候看到相关代码。 8. 异步克隆物体 为了异步克隆物体,我们会用到三个新的变量,我们用一个List<long>在记录异步加载任务的asyncGuid,接着这个asyncGuid是一个循环自增的long值。 对于异步克隆接口来说,与同步克隆接口相比,只是多了一个参数作为回调 如上图,假如对象池中已经有这个对象,那我们直接返回使用并触发回调。 如上图,假如对象没有被加载过,那么我们就会获取一个新的asyncGuid值,并将其加入到mAsyncLoadingTaskList中作为标识,而后调用异步加载资源的接口,在加载完成后则在mAsyncLoadingTaskList移除其,并对其进行实例化和执行回调。 9. 销毁克隆物体 如上图,我们将会通过获取GameObject的instance值,来获取其CacheObject对象。 上图是destory为true的情况,此时我们会直接Destory这个GameObject,而后尝试将其从对象池子中移除,如果对象池子不存在或者数量为0,那说明这个prefab已经没有任何地方在使用了,此时就直接做卸载资源的操作。 上图是destory为false的情况,此时我们就会把GameObject放到对象池子中,留着下次使用。 总结 我们来梳理一下资源加载的流程: 获取AB包的路径 通过配置文件加载AB包信息到字典中 加载AB包 同步加载资源 异步加载资源 卸载AB包和资源 同步克隆物体 异步克隆物体 销毁克隆物体 看到这里,恭喜你已经把整个资源框架的重点有个认识了,其实最后还有关于深度清理和其他不同类型的资源加载接口的编写两个知识点,都是比较简单的,但有了上面的基础后,花点时间阅读下源码,你会有更多收获的。那么这个系列也到此完成了,因为我个人第一次更这么长的系列,而且我一开始对于怎么写作这个没什么头绪,基本上这四章都是用图片加阐述源码的方式,但是想了下,感觉之后可以用一些文字概况归纳,而后让大家自己去阅读源码这种形式可能会更好一些。如果有精力的话,大概会弄重置版吧(