CocosCreator 유 니 버 설 프레임 워 크 디자인 의 자원 관리

만약 에 Cocos Creator 를 사용 하여 규모 가 약간 큰 게임 을 만 들 고 싶다 면 자원 관 리 는 반드시 해결 해 야 할 문제 입 니 다.게임 이 진행 되면 서 게임 의 메모리 점용 이 오 르 기만 하고 떨 어 지지 않 는 다 는 것 을 알 게 될 것 입 니 다.비록 현재 아주 적은 자원 만 사용 하고 cc.loader.release 를 사용 하여 이전에 불 러 온 자원 을 방출 하 더 라 도.하지만 이전에 사 용 했 던 대부분의 자원 은 메모리 에 남아 있 습 니 다!왜 이러 지?
cocos creator 자원 관리 에 존재 하 는 문제점
자원 관 리 는 주로 세 가지 문 제 를 해결 하고 자원 로드,자원 찾기(사용),자원 방출 을 해결 합 니 다.여기 서 토론 하고 자 하 는 것 은 주로 자원 방출 문제 이다.이 문 제 는 매우 간단 해 보이 고 Cocos2d-x 에서 도 간단 하지만 js 에서 복잡 해 졌 다.자원 이 방출 될 수 있 는 지 추적 하기 어렵 기 때문이다.
Cocos2d-x 에서 우 리 는 인용 계 수 를 사용 합 니 다.인용 계수 가 0 일 때 자원 을 방출 하고 인용 계 수 를 잘 유지 하면 됩 니 다.또한 Cocos2d-x 에서 우 리 는 자원 에 대한 관리 가 비교적 분산 되 어 있 습 니 다.엔진 차원 에서 TextureCache,AudioManager 와 같은 단일 사례 만 제공 하여 특정한 자원 을 관리 하고 대부분 자원 은 우리 가 스스로 관리 해 야 합 니 다.한편,cocos creator 에서 우리 의 자원 은 cc.loader 에 의 해 통일 적 으로 관리 되 고 prefab,prefab 와 각종 자원 의 복잡 한 인용 관 계 를 대량으로 사용 하여 자원 관리의 어려움 을 증가 시 켰 다.
자원 의존
자원 A 는 자원 B,C,D 에 의존 할 수 있 고 자원 D 는 자원 E 에 의존 할 수 있 습 니 다.이것 은 흔히 볼 수 있 는 자원 의존 상황 입 니 다.만약 에 우리 가cc.loader.loadRes("A")자원 A 를 불 러 오 면 B~E 는 모두 불 러 옵 니 다.그러나 우리 가 호출cc.loader.release("A")하면 자원 A 만 방출 됩 니 다.

불 러 오 는 모든 자원 은 cc.loader 의cache 에 서 는 cc.loader.release 가 들 어 오 는 자원 을 방출 할 뿐 자원 의존 을 고려 하지 않 습 니 다.
cc.loader 뒤의 자원 로드 프로 세 스에 관심 이 있다 면 참고 하 십시오.https://www.cnblogs.com/ybgame/p/10576884.html
만약 에 우리 가 의존 하 는 자원 도 함께 방출 하 기 를 원한 다 면 cocos creator 는 서 툰 방법 을 제공 합 니 다.cc.loader.getDependsRecursively;지정 한 자원 이 의존 하 는 모든 자원 을 재 귀적 으로 가 져 와 하나의 배열 에 넣 고 돌아 온 다음 에 cc.loader.release 에 이 배열 을 전송 합 니 다.cc.loader 는 이 를 옮 겨 다 니 며 하나씩 방출 합 니 다.
이런 방식 은 자원 을 방출 할 수 있 지만 방출 하지 말 아야 할 자원 을 방출 할 수 있다.만약 에 자원 F 가 D 에 의존 하면 이 럴 때 F 자원 이 정상적으로 작 동 하지 못 하 게 된다.cocos creator 엔진 이 자원 의 의존 을 잘 유지 하지 못 해서 D 를 방출 할 때 F 가 우리 에 게 의존 하 는 지 몰 랐 습 니 다.F 의존 이 없 더 라 도 D 를 방출 할 수 있 는 지 여 부 는 확실 하지 않 습 니 다.예 를 들 어 우 리 는 cc.loader 로 D 를 불 러 온 다음 에 A 를 불 러 왔 습 니 다.이때 D 가 불 러 왔 고 A 는 직접 사용 할 수 있 습 니 다.그러나 A 를 방출 할 때 D 도 방출 한다 면 이것 은 우리 의 기대 에 부합 되 지 않 는 다.우리 가 기대 하 는 것 은 우리 가 D 를 명시 적 으로 방출 하지 않 을 때 D 는 다른 자원 의 방출 에 따라 자동 으로 방출 되 지 말 아야 한 다 는 것 이다.
간단하게 테스트 할 수 있 습 니 다.Chrome 개발 자 모드 를 열 고 Console 패 널 에 입력 할 수 있 습 니 다.오래된 버 전의 cocos creator 라면 cc.textureCache 에서 dump 모든 무늬 를 사용 할 수 있 습 니 다.새 버 전 은 textureCache 를 제외 하고 cc.loader. 를 입력 할 수 있 습 니 다.cache 에서 모든 자원 을 확인 합 니 다.자원 이 너무 많 으 면 수량 에 만 관심 을 가지 고 Object.keys(cc.loader. 를 입력 할 수 있 습 니 다.cache).length 는 자원 의 총 수 를 볼 수 있 습 니 다.우 리 는 자원 을 불 러 오기 전에 dump 를 한 번,불 러 온 후에 dump 를 한 번,방출 한 후에 다시 dump 를 해서 cc.loader 의 캐 시 상 태 를 비교 할 수 있 습 니 다.물론 덤 프 그림 이나 덤 프 와 지난번 덤 프 의 차이 점 등 편리 한 방법 도 쓸 수 있다.

자원 사용
자원 의존 문 제 를 제외 하고 우 리 는 자원 사용 문 제 를 해결 해 야 한다.전 자 는 cc.loader 내부 의 자원 조직 문제 이 고 후 자 는 응용 층 논리의 자원 사용 문제 이다.예 를 들 어 우리 가 한 인터페이스 가 닫 힐 때 특정한 자원 을 방출 해 야 하 는 것 도 방출 해 야 하 는 문제 에 직면 할 것 이다.예 를 들 어 다른 닫 히 지 않 은 인터페이스 가 이 자원 을 사용 하 는 지 여부 이다.만약 다른 곳 에서 이 자원 을 사용 했다 면,그것 을 방출 해 서 는 안 된다!
ResLoader
여기 서 저 는 ResLoader 를 설계 하여 cc.loader 가 해결 하지 못 한 문 제 를 해결 하 였 습 니 다.관건 은 모든 자원 에 CacheInfo 를 만들어 자원 의 의존 과 사용 등 정 보 를 기록 하 는 것 입 니 다.이 를 통 해 자원 의 방출 여 부 를 판단 하고 ResLoader.getInstance().loadRes()를 사용 하여 cc.loader.loadRes()를 대체 하 는 것 입 니 다.ResLoader.getInstance().releaseRes()는 cc.loader.releaseRes()를 대체 합 니 다.
의존 에 대해 서 는 자원 을 불 러 올 때 ResLoader 가 자동 으로 맵 을 만 들 고 자원 을 방출 할 때 자동 으로 맵 을 취소 하 며 맵 을 취소 한 자원 이 방출 될 수 있 는 지 확인 하 는 것 이 방출 된 논리 입 니 다.
사용 에 대해 use 파 라 메 터 를 제공 합 니 다.이 파 라 메 터 를 통 해 이 자원 을 어디서 사 용 했 는 지,그리고 다른 곳 에서 이 자원 을 사 용 했 는 지 구별 합 니 다.한 자원 이 다른 자원 에 의존 하지 않 고 다른 논리 에 의 해 사용 되 지 않 으 면 이 자원 은 방출 될 수 있 습 니 다.

/**
 *      
 * 1.              ,  DependKeys      
 * 2.       ,     UI   A  ,        B,  B     A,          A   ,     A   ,
 * 3.           (             ,           ,        )
 * 
 * 2018-7-17 by   
 */

//          
export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
//          
export type CompletedCallback = (error: Error, resource: any) => void;

//          
interface CacheInfo {
    refs: Set<string>,
    uses: Set<string>
}

// LoadRes       
interface LoadResArgs {
    url: string,
    type?: typeof cc.Asset,
    onCompleted?: CompletedCallback,
    onProgess?: ProcessCallback,
    use?: string,
}

// ReleaseRes       
interface ReleaseResArgs {
    url: string,
    type?: typeof cc.Asset,
    use?: string,
}

//      
let isChildClassOf = cc.js["isChildClassOf"]
if (!isChildClassOf) {
    isChildClassOf = cc["isChildClassOf"];
}

export default class ResLoader {

    private _resMap: Map<string, CacheInfo> = new Map<string, CacheInfo>();
    private static _resLoader: ResLoader = null;
    public static getInstance(): ResLoader {
        if (!this._resLoader) {
            this._resLoader = new ResLoader();
        }
        return this._resLoader;
    }

    public static destroy(): void {
        if (this._resLoader) {
            this._resLoader = null;
        }
    }

    private constructor() {

    }

    /**
     *  cc.loader        item
     * @param url    url
     * @param type        
     */
    private _getResItem(url: string, type: typeof cc.Asset): any {
        let ccloader: any = cc.loader;
        let item = ccloader._cache[url];
        if (!item) {
            let uuid = ccloader._getResUuid(url, type, false);
            if (uuid) {
                let ref = ccloader._getReferenceKey(uuid);
                item = ccloader._cache[ref];
            }
        }
        return item;
    }

    /**
     * loadRes        
     */
    private _makeLoadResArgs(): LoadResArgs {
        if (arguments.length < 1 || typeof arguments[0] != "string") {
            console.error(`_makeLoadResArgs error ${arguments}`);
            return null;
        }
        let ret: LoadResArgs = { url: arguments[0] };
        for (let i = 1; i < arguments.length; ++i) {
            if (i == 1 && isChildClassOf(arguments[i], cc.RawAsset)) {
                //           type
                ret.type = arguments[i];
            } else if (i == arguments.length - 1 && typeof arguments[i] == "string") {
                //            use
                ret.use = arguments[i];
            } else if (typeof arguments[i] == "function") {
                //        
                if (arguments.length > i + 1 && typeof arguments[i + 1] == "function") {
                    ret.onProgess = arguments[i];
                } else {
                    ret.onCompleted = arguments[i];
                }
            }
        }
        return ret;
    }

    /**
     * releaseRes        
     */
    private _makeReleaseResArgs(): ReleaseResArgs {
        if (arguments.length < 1 || typeof arguments[0] != "string") {
            console.error(`_makeReleaseResArgs error ${arguments}`);
            return null;
        }
        let ret: ReleaseResArgs = { url: arguments[0] };
        for (let i = 1; i < arguments.length; ++i) {
            if (typeof arguments[i] == "string") {
                ret.use = arguments[i];
            } else {
                ret.type = arguments[i];
            }
        }
        return ret;
    }

    /**
     *         Key
     * @param where      , Scene、UI、Pool
     * @param who    , Login、UIHelp...
     * @param why     ,   ...
     */
    public static makeUseKey(where: string, who: string = "none", why: string = ""): string {
        return `use_${where}_by_${who}_for_${why}`;
    }

    /**
     *         
     * @param key       url
     */
    public getCacheInfo(key: string): CacheInfo {
        if (!this._resMap.has(key)) {
            this._resMap.set(key, {
                refs: new Set<string>(),
                uses: new Set<string>()
            });
        }
        return this._resMap.get(key);
    }

    /**
     *       
     * @param url             url
     * @param type              ,   null
     * @param onProgess           
     * @param onCompleted         
     * @param use               key,  makeUseKey    
     */
    public loadRes(url: string, use?: string);
    public loadRes(url: string, onCompleted: CompletedCallback, use?: string);
    public loadRes(url: string, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
    public loadRes(url: string, type: typeof cc.Asset, use?: string);
    public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use?: string);
    public loadRes(url: string, type: typeof cc.Asset, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
    public loadRes() {
        let resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments);
        console.time("loadRes|"+resArgs.url);
        let finishCallback = (error: Error, resource: any) => {
            //       (                    )
            let addDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        //          
                        this.getCacheInfo(depKey).refs.add(refKey);
                        // cc.log(`${depKey} ref by ${refKey}`);
                        let ccloader: any = cc.loader;
                        let depItem = ccloader._cache[depKey]
                        addDependKey(depItem, refKey)
                    }
                }
            }

            let item = this._getResItem(resArgs.url, resArgs.type);
            if (item && item.url) {
                addDependKey(item, item.url);
            } else {
                cc.warn(`addDependKey item error1! for ${resArgs.url}`);
            }

            //            
            if (item) {
                let info = this.getCacheInfo(item.url);
                info.refs.add(item.url);
                //       
                if (resArgs.use) {
                    info.uses.add(resArgs.use);
                }
            }

            //       
            if (resArgs.onCompleted) {
                resArgs.onCompleted(error, resource);
            }
            console.timeEnd("loadRes|"+resArgs.url);
        };

        //          
        let res = cc.loader.getRes(resArgs.url, resArgs.type);
        if (res) {
            finishCallback(null, res);
        } else {
            cc.loader.loadRes(resArgs.url, resArgs.type, resArgs.onProgess, finishCallback);
        }
    }

    /**
     *     
     * @param url       url
     * @param type      
     * @param use           key,  makeUseKey    
     */
    public releaseRes(url: string, use?: string);
    public releaseRes(url: string, type: typeof cc.Asset, use?: string)
    public releaseRes() {
        /**        */
        // return;

        let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
        let item = this._getResItem(resArgs.url, resArgs.type);
        if (!item) {
            console.warn(`releaseRes item is null ${resArgs.url} ${resArgs.type}`);
            return;
        }
        cc.log("resloader release item");
        // cc.log(arguments);
        let cacheInfo = this.getCacheInfo(item.url);
        if (resArgs.use) {
            cacheInfo.uses.delete(resArgs.use)
        }
        this._release(item, item.url);
    }

    //       
    private _release(item, itemUrl) {
        if (!item) {
            return;
        }
        let cacheInfo = this.getCacheInfo(item.url);
        //           
        cacheInfo.refs.delete(itemUrl);

        if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
            //     
            let delDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        let ccloader: any = cc.loader;
                        let depItem = ccloader._cache[depKey]
                        this._release(depItem, refKey);
                    }
                }
            }
            delDependKey(item, itemUrl);
            //    uuid,     url
            if (item.uuid) {
                cc.loader.release(item.uuid);
                cc.log("resloader release item by uuid :" + item.url);
            } else {
                cc.loader.release(item.url);
                cc.log("resloader release item by url:" + item.url);
            }
        }
    }

    /**
     *            
     * @param url   url
     * @param type      
     * @param use           key,  makeUseKey    
     */
    public checkReleaseUse(url: string, use?: string): boolean;
    public checkReleaseUse(url: string, type: typeof cc.Asset, use?: string): boolean
    public checkReleaseUse() {
        let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
        let item = this._getResItem(resArgs.url, resArgs.type);
        if (!item) {
            console.log(`cant release,item is null ${resArgs.url} ${resArgs.type}`);
            return true;
        }

        let cacheInfo = this.getCacheInfo(item.url);
        let checkUse = false;
        let checkRef = false;

        if (resArgs.use && cacheInfo.uses.size > 0) {
            if (cacheInfo.uses.size == 1 && cacheInfo.uses.has(resArgs.use)) {
                checkUse = true;
            } else {
                checkUse = false;
            }
        } else {
            checkUse = true;
        }

        if ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url)) || cacheInfo.refs.size == 0) {
            checkRef = true;
        } else {
            checkRef = false;
        }

        return checkUse && checkRef;
    }
}
ResLoader 사용
ResLoader 의 사용 은 매우 간단 합 니 다.다음은 간단 한 예 입 니 다.우 리 는 dump 단 추 를 누 르 면 현재 자원 의 총 수 를 볼 수 있 습 니 다.cc.load,cc.release 를 누 른 후에 각각 dump 를 한 번 누 르 면 36 개의 자원 이 있 고 로드 한 후에 40 개의 자원 이 있 으 며 방출 을 실행 한 후에 39 개의 자원 이 있 습 니 다.한 개의 자원 만 방출 되 었 습 니 다.
만약 에 ResLoader 를 사용 하여 테스트 를 하면 방출 된 후에 34 개의 자원 만 있 는 것 을 발견 할 수 있 습 니 다.이것 은 앞에서 불 러 온 장면 의 자원 도 이 테스트 자원 에 의존 하기 때문에 이런 자원 도 방출 되 었 습 니 다.우리 가 모두 ResLoader 를 사용 하여 자원 을 불 러 오고 마 운 트 해제 하면 자원 유출 문제 가 발생 하지 않 습 니 다.

예제 코드:

@ccclass
export default class NetExample extends cc.Component {
    @property(cc.Node)
    attachNode: cc.Node = null;
    @property(cc.Label)
    dumpLabel: cc.Label = null;

    onLoadRes() {
        cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
            if (!error) {
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    onUnloadRes() {
        this.attachNode.removeAllChildren(true);
        cc.loader.releaseRes("Prefab/HelloWorld");
    }

    onMyLoadRes() {
        ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
            if (!error) {
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    onMyUnloadRes() {
        this.attachNode.removeAllChildren(true);
        ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
    }

    onDump() {
        let Loader:any = cc.loader;
        this.dumpLabel.string = `      :${Object.keys(Loader._cache).length}`;
    }
}
위의 예 를 보면 노드 를 먼저 제거 한 다음 에 방출 하 는 것 을 볼 수 있다.이것 은 정확 한 사용 방식 이다.만약 에 내 가 제거 하지 않 고 직접 방출 한다 면?텍 스 처 를 방출 했 기 때문에 cocos creator 는 다음 렌 더 링 에서 계속 오 류 를 보고 합 니 다.
ResLoader 는 하나의 기초 일 뿐 ResLoader 를 직접 사용 할 때 우 리 는 자원 의 의존 문제 에 관심 을 가 질 필요 가 없습니다.그러나 자원 의 사용 문 제 는 우리 가 관심 을 가 져 야 합 니 다.실제 사용 에서 우 리 는 자원 의 생명 주기 가 다음 과 같은 몇 가지 상황 이 기 를 바 랄 수 있 습 니 다.
  • 특정한 대상 의 생명 주기 에 따라 대상 이 소각 할 때 자원 방출
  • 특정한 인터페이스의 생명 주기 에 따라 인터페이스 가 닫 힐 때 자원 방출
  • 특정한 장면 의 생명 주기 에 따라 장면 전환 시 자원 방출
  • 이 대상 이나 이 대상 의 다른 구성 요소 에서 논 리 를 작성 하고 자원 을 불 러 올 때 이 자원 관리 구성 요 소 를 사용 하여 불 러 올 수 있 습 니 다.이 구성 요 소 는 자원 의 방출 을 유지 합 니 다.인터페이스 와 장면 도 유사 하 다.
    프로젝트 코드 위치:https://github.com/wyb10a10/cocos_creator_framework,Scene 디 렉 터 리 의 ResExample 장면 을 열 면 볼 수 있 습 니 다.
    이상 은 바로 CocosCreator 유 니 버 설 프레임 워 크 디자인 의 자원 관리 에 대한 상세 한 내용 입 니 다.CocosCreator 프레임 워 크 디자인 의 자원 관리 에 관 한 자 료 는 저희 의 다른 관련 글 을 주목 해 주 십시오!

    좋은 웹페이지 즐겨찾기