본문 바로가기

IT/Tizen

[Tizen] 타이젠 개발 Z1, Z3 앱 라이프사이클의 이벤트 핸들링


안녕하세요, 타이젠 개발자 윤진입니다.


타이젠 데브랩을 진행할 때 빠지지 않고 언급했던 부분은,

애플리케이션의 기본 골격이라 여겨지는 라이프사이클입니다.

라이프사이클이 앱을 구성하는 필수적인 요소라는 것에는 재론의 여지가 없습니다.

중급개발자로 나아가기 위해서는 라이프사이클을 제대로 활용해야합니다.


하지만, 타이젠 스타터를 위한 데브랩에서 라이프사이클에 대한 설명이 필요할지 회의가 드네요.

애플리케이션을 작성할 때에는 분명히 유효한 개념일지는 모르지만,

처음 개발을 하는 사람들에게는 진입장벽만 높이고 있겠지요.

그래서 향후 데브랩에서는 따분한 라이프사이클, 이벤트핸들링, edc 따위는 날려버리려 합니다.

대신 네이티브 앱 작성이 얼마나 쉬운지 위주로 알려드릴 생각입니다. :)

(EFL 창시자 하이츨러 마스터와 함께 나눈 생각임을 알려드립니다.)


그 대신 블로그에서 마음껏 하고 싶은 얘기를 떠들 생각입니다.

네이티브 애플리케이션을 개발할 때 필수적인 개념들을,

하나씩 최대한 자세하게 짚으면서 풀어낼 예정입니다.

타이젠이 오픈소스인 이상,

소스단까지 파고들어가 동작원리와 코드리뷰를 하는 것도 좋겠네요.

그리고 소박하게나마 아키텍쳐 설계원칙도 제가 아는 선에서 언급하도록 하겠습니다.


어쨌든,

이번에는 이벤트 핸들링에 대해서 언급해볼까 합니다.


https://developer.tizen.org/community/tip-tech/application-fundamentals-developer-guide


이벤트 핸들링과 관련하여 개략적인 블록다이어그램입니다.

다이어그램에는 여러 낯선 용어들이 등장합니다.

AUL, AUL daemon, Application Information DB.


첫번째로 등장하는 AUL은 Application Utility Libray를 의미합니다.

한 앱에서 다른 앱을 런칭 혹은 리쥼 혹은 종료시킬 때 사용하는 API를 제공합니다.

AUL에서 제공하는 API는 "aul_" prefix를 가지고 있습니다.

타이젠 SDK에서 "aul_"로 시작하는 API를 발견하셨나요?


물론 발견하실 수 없을 겁니다. :(

왜냐하면 AUL API들은 플랫폼 내부에서 사용하는 함수군이기 때문입니다.


대신 외부에 오픈되어 있는 API로 app_control API가 있습니다.

app_control API 내에서 AUL을 사용하여 다른 앱을 컨트롤하게 됩니다.

app_control은 사용하기 쉽게 재가공한 API로 외부개발자들에게 노출되어 있습니다.

반면 AUL은 플랫폼 내부 피쳐로 외부에 노출할 필요가 없는 API도 관리하고 있습니다.

app_control repo. : platform/core/api/application


int app_request_to_launchpad_with_fd(int cmd, const char *appid, bundle *kb, int *fd, int uid)
{
	int must_free = 0;
	int ret = 0;

	SECURE_LOGD("launch request : %s", appid);
	if (kb == NULL) {
		kb = bundle_create();
		must_free = 1;
	} else
		__clear_internal_key(kb);

	ret = __app_send_cmd_with_fd(AUL_UTIL_PID, uid, cmd, kb, fd);

	_D("launch request result : %d", ret);
	if (ret == AUL_R_LOCAL) {
		_E("app_request_to_launchpad : Same Process Send Local");
		bundle *b;

		switch (cmd) {
			case APP_START:
			case APP_START_RES:
				b = bundle_dup(kb);
				ret = __app_launch_local(b);
				break;
			case APP_OPEN:
			case APP_RESUME:
			case APP_RESUME_BY_PID:
				ret = __app_resume_local();
				break;
			default:
				_E("no support packet");
		}

	}

	/*   cleanup */
	if (must_free)
		bundle_free(kb);

	return ret;
}


앱을 런칭하면 app_control에서 AUL로 런칭요청이 넘어오고,

결국 위의 API가 실행되게 됩니다.


위의 API 이름을 보면 여러가지 정보를 얻을 수 있지요.

app(앱)이 request(요청)를 하고 있는데,

launchpad(런치패드)라는 데몬에게 fd(파일 디스크립터)를 실어서 보내고 있군요.

여기서 launchpad가 바로 위의 블록다이어그램에 언급된 AUL daemon입니다.

Caller 앱은 미리 열어둔 유닉스 소켓을 사용하여 launchpad에게 요청을 전달합니다.

(socket과 관련된 내용은 aul-1/src/app_sock.c에서 살펴보실 수 있습니다.)


요청을 전달받은 launchpad는 caller 앱의 요청을 수행하기 전에,

caller앱에게 합당한 권한이 있는지 살펴봅니다.

합당한 권한이 있다면, caller 앱의 요청사항에 따라 callee 앱을 런칭/리쥼/종료하게 되고,

합당한 권한이 없다면, 아무 것도 수행하지 않습니다.


caller 앱의 요청이 callee 앱을 런칭시키는 것이라면,

런치패드의 한 모듈인 런치패드 로더에서,

사전에 fork/exec으로 만들어 놓은 프로세스를 활용하여 callee 앱을 띄웁니다.

fork/exec 자체가 상당한 시간을 소요하기 때문에,

앱 런칭 성능 향상을 위해 사전에 프로세스를 만들어놓지요.


static void __init_window(void)
{
	Evas_Object *win = elm_win_add(NULL, "package_name", ELM_WIN_BASIC);
	if (win) {
		aul_set_preinit_window(win);

		Evas_Object *bg = elm_bg_add(win);
		if (bg) {
			evas_object_size_hint_weight_set(bg, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
			elm_win_resize_object_add(win, bg);
			aul_set_preinit_background(bg);
		} else {
			_E("[candidate] elm_bg_add() failed");
		}

		Evas_Object *conform = elm_conformant_add(win);
		if (conform) {
			evas_object_size_hint_weight_set(conform, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
			elm_win_resize_object_add(win, conform);
			aul_set_preinit_conformant(conform);
		} else {
			_E("elm_conformant_add() failed");
		}
	} else {
		_E("[candidate] elm_win_add() failed");
	}
}

fork/exec 뿐만 아니라 위의 코드에서 볼 수 있듯,

- 윈도우 elm_win_add()

- 백그라운드 elm_bg_add()

- 컨포먼트 elm_conformant_add()

앱을 구성하는 필수적인 요소들을 사전에 만들어놓습니다.


static void __adapter_add_fd(void *user_data, int fd,
                             loader_receiver_cb receiver)
{
	__fd_handler = ecore_main_fd_handler_add(fd,
			(Ecore_Fd_Handler_Flags)(ECORE_FD_READ | ECORE_FD_ERROR),
			__process_fd_handler, NULL, NULL, NULL);
	if (__fd_handler == NULL) {
		_D("fd_handler is NULL");
		close(fd);
		exit(-1);
	}

	__receiver = receiver;
}

그리고 위에서 볼 수 있듯,

mainloop에 진입하기 전에 file descriptor도 등록하게 됩니다.

file descriptor를 통해 mainloop에 진입한 후에도,

fd로 넘어오는 이벤트를 처리할 수 있게 됩니다.

ecore_main_fd_handler_add()의 역할이 mainloop에서도 fd 이벤트를 처리해주는 것이지요.


자, 이제 앱을 런칭시킬 모든 준비가 끝났습니다.

윈도우 등을 비롯한 UI 컴포넌트도 만들어두고,

이벤트 전달을 위한 fd handler도 생성해두었습니다.

그렇다면 이제는 실제 callee 앱을 얹힐 차례입니다.


static int __loader_terminate_cb(int argc, char **argv, void *user_data)
{
	void *handle = NULL;
	int res;
	int (*dl_main)(int, char **);

	SECURE_LOGD("[candidate] Launch real application (%s)", argv[0]);
	handle = dlopen(argv[0], RTLD_LAZY | RTLD_GLOBAL);
	if (handle == NULL) {
		_E("dlopen failed(%s). Please complile with -fPIE and link with -pie flag",
			dlerror());
		goto do_exec;
	}

	dlerror();

	dl_main = dlsym(handle, "main");
	if (dl_main != NULL)
		res = dl_main(argc, argv);
	else {
		_E("dlsym not founded(%s). Please export 'main' function", dlerror());
		dlclose(handle);
		goto do_exec;
	}

	dlclose(handle);
	return res;

do_exec:
	if (access(argv[0], F_OK | R_OK)) {
		char err_str[MAX_LOCAL_BUFSZ] = { 0, };

		SECURE_LOGE("access() failed for file: \"%s\", error: %d (%s)",
			argv[0], errno, strerror_r(errno, err_str, sizeof(err_str)));
	} else {
		SECURE_LOGD("[candidate] Exec application (%s)", g_argv[0]);
		if (execv(argv[0], argv) < 0) {
			char err_str[MAX_LOCAL_BUFSZ] = { 0, };

			SECURE_LOGE("execv() failed for file: \"%s\", error: %d (%s)",
				argv[0], errno, strerror_r(errno, err_str, sizeof(err_str)));
		}
	}

	return -1;

}

위의 코드 상단부에 보면 dlsym으로 main 함수를 여는 부분이 있습니다.

바로 저기에서 런치패드 로더는 비로소 앱의 컨택스트를 프로세스에 탑재하게 됩니다.


앱의 컨텍스트는 application API를 사용하여 등록한 앱 라이프사이클을 의미합니다.

앱 개발자가 앱 라이프사이클을 등록할 때에는 application API를 사용하는데요,

application API는 내부적으로 app-core를 사용하지요.


EXPORT_API int appcore_efl_main(const char *name, int *argc, char ***argv,
				struct appcore_ops *ops)
{
	int r;

	r = appcore_efl_init(name, argc, argv, ops);
	_retv_if(r == -1, -1);

	elm_run();

	appcore_efl_fini();

	return 0;
}

위처럼 callee앱이 Application API를 사용하여 채워넣은 라이프사이클 정보-

create, terminate, resume, pause, control-는 appcore_ops라는 구조체로 app-core 쪽에 전달하게 됩니다.


static void __add_climsg_cb(struct ui_priv *ui)
{
	_ret_if(ui == NULL);
#if defined(WAYLAND)
	ui->hshow =
		ecore_event_handler_add(ECORE_WL_EVENT_WINDOW_SHOW, __show_cb, ui);
	ui->hhide =
		ecore_event_handler_add(ECORE_WL_EVENT_WINDOW_HIDE, __hide_cb, ui);
	ui->hvchange =
		ecore_event_handler_add(ECORE_WL_EVENT_WINDOW_VISIBILITY_CHANGE,
				__visibility_cb, ui);
	ui->hlower =
		ecore_event_handler_add(ECORE_WL_EVENT_WINDOW_LOWER,
				__lower_cb, ui);
#elif defined(X11)
	ui->hshow =
		ecore_event_handler_add(ECORE_X_EVENT_WINDOW_SHOW, __show_cb, ui);
	ui->hhide =
		ecore_event_handler_add(ECORE_X_EVENT_WINDOW_HIDE, __hide_cb, ui);
	ui->hvchange =
		ecore_event_handler_add(ECORE_X_EVENT_WINDOW_VISIBILITY_CHANGE,
				__visibility_cb, ui);

	/* Add client message callback for WM_ROTATE */
	if (!__check_wm_rotation_support()) {
		ui->hcmsg = ecore_event_handler_add(ECORE_X_EVENT_CLIENT_MESSAGE,
				__cmsg_cb, ui);
		ui->wm_rot_supported = 1;
		appcore_set_wm_rotation(&wm_rotate);
	}
#endif
}

그리고 각각의 라이프사이클 콜백 함수들을,

각각의 동작원리에 따라 App-core에서 관리하게 됩니다.

위의 코드에는 라이프사이클 중 resume/pause 이벤트가 나와있네요.

윈도우에 show/hide 콜백을 걸어놓고 resume/pause을 매칭시키죠.


이런 식으로 라이프사이클까지 등록까지 완료되면,

앱런칭을 위해 create 콜백, control 콜백, resume 콜백이 차례대로 불러줍니다.

그러면 최종적으로 UI를 갖춘 앱이 런칭이 되겠지요. :)


자, 여기까지-

- Application

- AUL

- App-core

- Launchpad

위의 네가지가 합작하여 앱을 런칭시키는 과정을 살펴보았습니다.


간단하게 보자면,

1. Application API의 app_control을 사용하여 callee 앱을 런칭하고자 시도

2. app_control API는 내부적으로 AUL을 이용하여 앱런칭 요청을 launchpad로 전달

3. launchpad는 사전에 process를 만들어 필요한 이벤트를 달아놓기

4. launchpad에서 미리 만든 process에 callee 앱의 main 함수를 dlsym으로 로드하기

5. callee main에서 Application API(내부적으로 App-core)를 사용하여 앱 라이프사이클 등록하기


위와 같이 요약할 수 있겠네요.

오래된 기억을 더듬어가며 썼기 때문에 최신의 구조에서는 변경된 항목이 있을 수도 있습니다.

자세한 내용보다는 전체적인 그림을 그리는데 이용하시면 좋겠습니다.


그럼 좋은 하루 보내세요~

끝_