도입
지금 맡은, 사내 프로젝트의 게임 클라이언트 앱은 약 2년 가까이 되어간다. ( 3년이라고 오타를 냈어서 수정했다.. ㅎㅎㅎ )
거기에, 굉장히 민첩한 속도로 평균적으로 1~2주에 1회의 업데이트를 쳐나갔다.
그렇기 때문에 당시에 쌓아둔 기술부채가 정말 많은데,
그 중 하나가 , MonoBehaviour 의 남용이다.
게임 내에 Main Scene 으로 진입할 때, 마지막 프레임에서 램이 512 MB 미만인 일부 기기들은
크래시가 발생하기도 하고, 다른 기기들도 대부분 일정 시간의 ANR 이 발생하기도 한다.
이번 포스팅은 , 해당 부분을 짧은 작업 시간내에 완화하는 것과 관련이 있다.
문제 파악
현재 보고하거나 인지된 문제들은 다음과 같았다.
-
씬 로딩 마지막에, 사실상 게임이 5초~ 10 초정도 멎고 나서, 게임이 로드된다.
-
일부 기기에서 씬 로드시 Memory가 터져서, 게임이 비정상 종료된다.
-
Awake()의 정확한 호출 순서를 정확히 파악하지 못했고, 공식 문서에도 “임의로” 라고 설명하듯, 새 객체를 짜서 넣을 때 예상치 못한 이슈가 일어난다.
애초에 MonoBehaviour 사용을 크게 줄이면 되겠지만, 그러려면 구조를 거의 새로 짜야하는 데다가,
방대한 컨텐츠를 테스트할 시간도 확보되지 않았다.
문제를 파악하고 2시간 내에 대략의 기반작업을 끝내야 했다.
특히 Monobehaviour.Awake 가 700개 정도 임의의 순서대로 작동하는 것이 주 원인이고,
개선 방향을 고민하다가, 중간에 살짝 잠들었는데 방법이 떠올라서 ( 나는 자주 이러더라. )
우선 급하게 Awake 의 부하가 씬 진입 1프레임 내에 몰리는 이슈를 해결하기로 했다.
대응
정상 작동하는 경우, 해당 부분의 Awake 호출 순서를 우선 파악하기 위해서
다음과 같은 스크립트를 사용했다.
using System;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
public class Tracer
{
public Stack<string> logStack = new Stack<string>();
public Stack<string> logStackTotal = new Stack<string>();
static Tracer instance;
public static Tracer Instance
{
get
{
if (instance == null)
{
instance = new Tracer();
}
return instance;
}
}
public void Log(MonoBehaviour targetBehaviour)
{
string objectName = targetBehaviour.GetType().ToString();
StackFrame stackFrame = new System.Diagnostics.StackTrace().GetFrame(1);
string fileName = stackFrame.GetFileName();
string methodName = stackFrame.GetMethod().ToString();
int lineNumber = stackFrame.GetFileLineNumber();
logStack.Push("\n" + objectName);
logStackTotal.Push("\n" + objectName + "\n" + " " + fileName + " " + methodName + " (" + lineNumber + ") ");
}
public void Save()
{
WriteLogFile("logStack.txt", logStack);
WriteLogFile("logTotalStack.txt", logStackTotal);
UnityEditor.AssetDatabase.Refresh();
UnityEngine.Debug.LogError("저장완료");
}
void WriteLogFile(string name, Stack<string> logStack)
{
string basePath = Application.dataPath;
string targetPath = Path.Combine(basePath, name);
if (File.Exists(targetPath))
{
File.Delete(targetPath);
}
UnityEngine.Debug.LogError(logStack.Count);
using (StreamWriter outputFile = new StreamWriter(targetPath))
{
foreach (var item in logStack)
{
outputFile.WriteLine(item);
}
}
}
}
간단히 + 무식하게 “Awake() {“ 를 가진 파일을 검색해서 “Awake() { Tracer.Instance.Log(this);” 와 치환해줬다.
그래서 결과적으로 만족할만한 작동 순서는 우선 얻었다.
결과물인 txt 파일을 팀에 공유하고 ,본격적인 작업에 들어갔다.
다음과 같은 인터페이스를 짜넣고 , 각 MonoBehaviour 상속자의 Awake 를 SerialAwake() 로 교체 했다.
Name 은 this.name 을 리턴하도록 했고, Priority 는 우선 급한 것만 넣고 나머지는 전부 뒤로 뺐다.
using System.Collections;
public interface ISerialStarter
{
string Name
{
get;
}
int Priority
{
get;
}
IEnumerator SerialAwake();
IEnumerator SerialStart();
}
그리곤 , 이 인터페이스들을 빌드 전에 수집하고 관리하는 매니저 클래스를 하나 파고, 그 클래스의 에디터도 커스텀 해줬다.
( 버튼 하나 누르면, 씬 내의 이 인터페이스(ISerialStarter)를 상속받은 객체들을 모두 가져와서 , 참조를 저장한다.)
저장된 ISerialStarter 들을 우선순위대로 실행한다. ( 실은 모두 가져올 때 한번 Sort )
기반 작업은 됐고, 문제도 없는 것 같지만 ,
객체 자체가 많다보니, 인형 눈알 붙이는 것처럼 적용 자체가 꽤 오래걸려서 ㅎㅎㅎㅎ
아직 마무리는 못한 상태다.
효과나 후기는 다음에 작성하기로 하고 마친다.