본문 바로가기
Tech/flutter

[flutter] Isolate, Compute. 화면 안버벅이고 큰 이벤트 실행하기

by 패드로 2021. 3. 16.

플러터(다트)는 단일 쓰레드를 사용합니다.

즉, 사람으로 치면 한 사람이 화면도 그리고, 데이터 통신도 수행하고, 유저의 행동에 따른 반응도 해주고, 그에 필요한 다양한 수학적 연산들도 수행하죠!

 

참 대단한 사람이죠?

 

하지만 한손으로 하늘을 가리지 못하듯, 점점 더 이 사람에게 많은 것을 요구할 경우 발생할 수 있는 문제가 있습니다.

바로 동시에 할 수 있는 일은 한정되어있다는거죠!

 

플러터는 60프레임, 혹은 기기에 따라 120프레임의 퍼포먼스를 목표로 하고 있습니다.

개발자로써는 범용앱이라면 항상 최저 디바이스에서도 원할하게 수행되는 앱을 만들 수 있어야 한다고 생각하기 때문에

60프레임에서 무난하게 돌아가는 앱이 되어야겠죠? 

이 경우 플러터가 매 16ms마다 한번씩 업데이트를 해야하는데, 바꿔 말하면 한 연산을 16ms 이상 수행하게 된다고 했을 때 아주 민감한 사람은 "화면이 버벅되네?(프레임 드랍이네? / 영어로는 Jank가 발생했네?)" 라는 걸 느낄 수 있다는 겁니다.

그럼 이럴 경우 어떻게 해야할까요?

 

직장에서 일을 할 때도 해오던 일이 혼자 못할 지경이 생기면 옆사람이나 옆부서에 헬프를 요청하겠죠?

마찬가지로 플러터에도 그러한 기능이 있습니다.

 

바로 Isolate. 

별도의 메모리를 가지고, 별도의 이벤트 루프를 가진 친구를 소환한다고 보시면 됩니다!

 

하지만, 옆 부서에 헬프를 한다고 바로 딱 와서 도와줄 수는 없겠죠?

어떤 업무를 도와달라고 할 건지, 옆 부서와 커뮤니케이션을 어떻게 할 건지 정의를 해줘야합니다.

 

일단 커뮤니케이션을 위해 '저'와 '옆팀 팀원'은 메일을 활용하기로 합니다.

옆팀 팀원은 메일 주소(ReceivePort)를 만들어서 지원할 내용을 전달해달라고 했고, 일이 끝나면 제가 메일을 보낸 메일 주소(SendPort)로 바로 답장을 해준다고 했습니다.

ReceivePort / SendPort

둘 다 메일을 보내는 개념이라, 수신자를 다중으로 수신해서 메일을 보낼 수도 있습니다.

 

메일이라고 설명을 했지만, Isolate간의 통신이 가능한 유일한 방법입니다.

 

이 메일을 보내기 위해서는 spawn이라고 하는 메소드를 사용해줍니다.

Isolate isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort);

 이를 설명하면, 새로운 Isolate을 만들어서 메일을 보낼건데, isolateFunction이라는 업무를 부탁할거고, 나중에 일일 끝나면 receivePort.sendPort로 답장해달라는 요청을 적은 겁니다.

 

전체 예제는 다음과 같습니다.

Isolate를 만들어주어 _runIsolate 작업을 부탁합니다.

Isolate.spawn의 두번째 인자는 사실상 포트 정보를 넣는 부분인데, List<String>의 형태로 입력받습니다.

"본래는!" 포트 정보만 넣어주는게 안정적인 사용이지만, 넘겨주어야할 데이터가 있다면 간단한 형태로 넘겨줄 수도 있습니다.

(혹시 더 좋은 방법이 있다면 알려주시면 감사하겠습니다)

그 후 _receivePort에서 응답을 기다리고있다가 메시지를 받게되면 _handleMessage를 실행시켜서 결과값에 대한 처리를 진행해줍니다.

 

handleMessage에서 포트를 닫고, isolate를 kill해주었는데, 반복적인 사용을 원하신다면 그렇게 하지 않고 반복적인 사용도 가능합니다.

runIsolate에서 반복적인 send를 보낼 때 마다 실행되니까요.

ReceivePort _receivePort;
Isolate _isolate;

void launchIsolate(){
	_receivePort = ReceivePort();
    //메일 받을 주소를 적고
    _isolate = await Isolate.spawn(_runIsolate, [
      _receivePort.sendPort,
      "something"
    ]);
    _receivePort.listen(_handleMessage);
    //결과값 받았을 때 _handleMessage를 실행
}

static void _runIsolate(List<Object> arguments) {
    SendPort sendPort = arguments[0];
    String message = arguments[1];
    
    //do someting
    
    sendPort.send(message);
    //모든게 끝나고 응답을 보내줍니다. 일반 함수의 return과 같다고 보면 되겠네요
 }
 
 void _handleMessage(dynamic data) {
    String answer = data.toString();
    print(answer); // => "something" 출력됨
    _receivePort.close(); // 포트 닫아주고
    _isolate.kill(priority: Isolate.immediate); // 다 끝났으니 종료
    _isolate = null; // 초기화
  }

 

이 일련의 과정들이 복잡하다고 느껴진다면(메일 주소 설정하고 받고하기 어렵다! 하신다면) 

compute를 써보시기 바랍니다.

 

마찬가지의 기능을 하지만 port에 대한 걱정을 할 필요가 없이 단순히 작성만 해주면 끝납니다.

늘 좋은게 있으면 나쁜게 있듯이, 훨씬 편하게 쓸 수 있는 대신 사용 가능한 기능들이 제한되는 부분은 있습니다.

(제한 기능: Isolate를 잠시 중단시키거나 kill 하는 등의 응용)

Future<List<String>> getString(String argument) async {
  // compute 함수를 사용하여 특정 함수를 별도 isolate에서 수행합니다.
  return compute(getTwoString, argument);
}

List<String> getTwoString(String text) {
  List<String> list = new List();
  list.add(text);
  list.add(text); //단순히 입력받은 string을 2배해서 리스트로 갖는 함수
  return list;
}

void main() async {
  List<String> result = await getString("a");
}

 

간단히 입력 받은 문자열을 복제하여 리스트 형태로 반환하는 예제를 짜보면 위와 같습니다.

Isolate.spawn을 썼을 때 보다 훨씬 간단하죠? Isolate의 기능을 쓰면서도 그냥 return 문을 쓸 수 있다는게 초보분들 입장에서는 헷갈리지 않겠다는 생각도 드네요.

 

*주의점 & 한계

compute에서 두번째 인자로 넘기는 변수나, Isolate.spawn의 두번째 인자의 리스트 중 하나로 넘기는 변수 모두 "간단한" 녀석이어야합니다. null, int, double, bool, String, List<객체나 원시타입>은 제대로 작동하는것 같은데, http response나 Future 같이 복잡한 녀석들은 에러를 발생시킵니다. 저같은 경우에는 Hive의 Box를 넘겨줘서 DB 작업을 수행하려고 했는데 에러를 발생시키더라구요.

최대한 주고받는 메시지는 간료화 시키면서 앱의 성능을 저하시키는 기능들을 몰아넣는 설계가 필요할 것 같습니다.

댓글