마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

앱 강제 종료시 이벤트를 발생하려면 어떻게 해야할까요?

0 추천

여기서 말하는 강제종료는 기기마다 다르겠지만 안드로이드 기기 아래에 기본적으로 있는 세 버튼중 첫번째 버튼을 누르면 실행 앱이 작아지고 테두리 상태가 되며, 그 상태에서 위나 옆으로 슬라드하여 종료하는 경우를 말합니다.​

제가 Log를 보면서 위와 같은 강제종료 상황을 지켜보았는데 해당 Activity나 Fragment의 onDestroy메소드가 호출이 되는 경우도 있고 안되는 경우도 있고, 호출이되더라도 첫줄만 실행되고 끝납니다. ​

제가 원하는건 강제종료가 되어도 아래코드와 같이 onDestroy메소드가 호출이되면서 서비스를 종료하게 하는 것인데, 서비스가 정상적으로 종료가 안되니 제가 원하는 결과들이 나오지 않더군요 ㅜㅜ​

혹시 앱을 저렇게 강제종료할 시 onDestroy()가 아닌 호출되는 다른 함수가 있나요? 아니면 강제종료시 이벤트 처리를 할 수 있는 다른 방법은 없을까요? 답변 부탁드립니다 !​

(앱 강제종료시 서비스만 따로 종료하는 방법이 있으면 그것을 알려주셔도 감사하겠습니다!!)

@Override

public void onDestroy() {

super.onDestroy();

Log.i("HomeFragment", "onDestroy 로 들어옴");

Intent intent = new Intent(getActivity(), MyService.class);

getActivity().stopService(intent);

}

lns0mnia (380 포인트) 님이 2020년 12월 20일 질문

1개의 답변

+2 추천
 
채택된 답변
이 문제를 해결하는 전통적인 방법은 3가지가 있습니다. 근데 더 좋은 방법도 있네요. (마지막 참고)

1. 항상 데이터를 저장합니다. 평상시에 데이터를 SharedPreference나 파일 혹은 DB에 보관합니다.
SharedPreference에 gson으로 저장하면, 객체가 저장되고, 다시 불러올 수 있습니다.

2. Service를 다른 process로 실행하면, activity가 죽어도 해당 서비스는 살아 있습니다. 이 서비스가 데이터를 처리하고 죽으면 됩니다.

3. 죽지않는 Foreground Service로 실행을 하면, 앱이 죽지 않습니다. (제일 안좋은 방법)

 

*** 구글링하니, 더 좋은 방법이 있네요. 2번하고 비슷하지만, 정말 쉽게 수정이 가능하네요.

https://stackoverflow.com/questions/19568315/how-to-handle-code-when-app-is-killed-by-swiping-in-android

<service
    android:name="com.myapp.MyService"
    android:stopWithTask="false" />

오우, 완전 대박인데요. 채택된 답변이니까, 확실합니다.

그러나 근본적으로는 1번 방식을 권고합니다. 언제든지 시스템이 둘 다 죽일 수 있습니다.
Will Kim (43,170 포인트) 님이 2020년 12월 20일 답변
lns0mnia님이 2020년 12월 22일 채택됨
일단 마지막 방법은 해봤는데 안되는거 같습니다 ㅜㅜ
저렇게 설정을 하고 강제종료를 해보면 서비스의 onTaskRemoved함수가 호출이 안되네요 ..

제가 지금 서비스 안에 SharedPreference를 구현을 해놓았는데 서비스의 onDestroy함수안에 SharedPreference로 데이터 저장을, onStartCommand 함수안에 데이터를 불러오는 코드를 넣어놨습니다. 그리고 현재는 메인 액티비티 안에 속해있는 프레그먼트의 onDestroy함수안에 제 질문글과 같이 stopService함수를 호출해 놓은 상태이구요 . 정상종료를 할때는 모든 코드가 잘 돌아가고 데이터도 잘 저장됩니다. 그런데 강제종료를 하면 프래그먼트의 onDestroy가 호출안될때가 있고 호출될때도 stopService를 실행하지 않고 종료되기 때문에 서비스의 데이터가 저장도 되지 않습니다. 말씀해주신 1번방법과 연관이 있을거 같아서 말씀드립니다 ... 어떤 문제의 가능성이 있을까요?
테스트해 보니 되는 것 같습니다.
다만, 4초 이상은 실행이 안됩니다.
아래 참고해 주세요~

일단, 링크에 onTaskRemoved는 @Override가 빠져있긴 하네요.

일단은 서비스를 하나 추가합니다. IntentService는 아니고, 그냥 일반 Service입니다.
Manifest에 다음과 같이 넣어줍니다.
        <service
            android:name=".MyService"
            android:enabled="true"
            android:stopWithTask="false">

        </service>

MainActivity에서 서비스를 실행합니다.
startService(new Intent(getBaseContext(), MyService.class));

제가 테스트한 서비스는 다음과 같습니다.
public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyService", "MyService Destroyed");
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        super.onTaskRemoved(rootIntent);

        for (int i=0; i < 1000000000; i++) {
            if (i % 1000000 == 0) Log.e("onTaskRemoved", "MyService: "+i);
        }
        stopSelf();
    }
}

1억까지 카운트 하는데, 2초 조금 넘게 걸리고, Destroyed 가 그 이후에 되었네요.

2020-12-22 03:21:35.440 3390-3390/com.appcognito.myapplication D/InputTransport: Input channel destroyed: '828492b', fd=69
2020-12-22 03:21:35.444 3390-3390/com.appcognito.myapplication E/onTaskRemoved: MyService: 0

...

2020-12-22 03:21:37.778 3390-3390/com.appcognito.myapplication E/onTaskRemoved: MyService: 999000000
2020-12-22 03:21:37.783 3390-3390/com.appcognito.myapplication D/MyService: MyService Destroyed

값을 더 크게 해 보니까, 4초까지는 실행이 되고, 그 이상은 죽네요.
4초 이상의 시간이 필요하다면 이 방법으론 안되겠네요.

2020-12-22 03:28:47.413 19822-19822/com.appcognito.myapplication D/InputTransport: Input channel destroyed: '9cc2448', fd=70
2020-12-22 03:28:47.414 19822-19822/com.appcognito.myapplication E/onTaskRemoved: MyService: 0

...

2020-12-22 03:28:51.216 19822-19822/com.appcognito.myapplication E/onTaskRemoved: MyService: 1410000000
2020-12-22 03:28:51.218 19822-19822/com.appcognito.myapplication I/Choreographer: Skipped 478 frames!  The application may be doing too much work on its main thread.
2020-12-22 03:28:51.219 19822-19822/com.appcognito.myapplication D/MyService: MyService Destroyed
자세한 설명 감사합니다 ㅠㅠ 저도 저렇게 샘플 service를 하나 만들고 위와같은 방법으로 해보니 실행이 되네요.. 근데 제가 하고자 하는 service는 아마 4초 이상의 처리가 필요하여 강제종료시 정상적으로 처리가 안되는것 같습니다.
 다른 방법으로 처리를 해야할것 같은데,
혹시 답변주신 1번방법에 대해서 조금더 자세히 알려주실 수 있으신가요??
서버랑 통신해서 받은 데이터를 보통 클래스의 인스턴스로 받아서 처리하죠.
그때 중요한 데이터일때, 혹은 매번 조회하지 않고, 이전에 통신 받은 데이터를 재활용할 때는, 인스턴스 자체를 SharedPreference에 저장했다가 필요할 때 다시 읽어 옵니다.

다음과 같은 공통 함수를 만들어서 사용하죠.

    public static User getUser(Context context) {
        String json = context.getSharedPreferences("_" + appName, MODE_PRIVATE).getString(USER_INFO, "");
        if (json == null || json.isEmpty())
            return null;
        Gson gson = new Gson();
        return gson.fromJson(json, User.class);
    }

    public static void setUser(Context context, User user) {
        Gson gson = new Gson();
        String json = gson.toJson(user);
        context.getSharedPreferences("_" + appName, MODE_PRIVATE).edit().putString(USER_INFO, json).apply();
    }

저장할 때는 User 객채를 받아서, Json String으로 Serialize 해서 문자열로 저장하고, 읽을 때는 String을 User 객체로 Deserialize해서 복원합니다.
저렇게 하면 중요 객체들을 앱이 죽었다 살아나도 유저의 id, 이름, 토큰 등등을 가져올 수 있죠. 서버 통신 없이말이죠.
실제로 저 객체에는 안에 배열도 들어가 있어서 아무리 댑스가 많아도 gson을 이용해 한번에 저장하고 가져옵니다. 속도가 느리지 않아요.
물론 데이터가 물리적으로 많으면 느리겠지만요.
한가지 우려가 되서 드리는 말씀인데, 유저 데이터를 SharedPreference에 저장하실 때는, 반드시 암호화를 하세요. 가능하면 안드로이드 Keystore를 이용해서 하시는 게 더 안전하겠죠. 루팅폰에만 앱을 깔아도 바로 사용자 정보가 노출되기 때문에 상당히 위험합니다.
그리고 onDestory는 드문 경우지만, 프로세스 킬이되거나 할 때 100% 호출된다는 보장이 없습니다. 이게 서비스나 리스너를 등록하고 해제할 때 onStart/onStop을 선호하는 이유입니다.확실하게 저장을 하고 싶다면 onStop이나 onPause가 적절한 이벤트입니다.
@spark 님이 Good Point를 말하셨네요, 당연히 패스워드는 저장하면 안되고요, 개인적인 중요한 정보는 Base64로 암호화 해서 보관하는게 좋겠습니다. 메모리상의 데이터도 암호화되어 있는 형태로 아예 접근하게 하는것도 좋은 방법입니다.

법적으로 보면,
루팅을 사용자가 직접했다면 자기 데이터이기 때문에 문제가 안될 것이고,
타인이 했다면 불법적인 것이죠. 불법으로 타인의 개인 정보를 취득할 방법은 디지털 포렌식 같은거로 많이 할 수 있죠. 암호화도 풀어내니까요.

법적으로는 굳이 할 필요는 없지만, 개발한 제품의 완성도적인 차원에서는 처리할 필요가 있다고 봅니다.

아래 참고하세요.

https://stackoverflow.com/questions/19556433/saving-byte-array-using-sharedpreferences
@spark
@Will Kim
두 분다 자세하고 좋은 답변들 정말 감사합니다ㅜㅜ 먼저 제가 구현하고자 하는 앱에서는 서버를 이용하지도 않고 회원정보 저장도 하지 않기 때문에 보안 문제는 우려가 안될 것으로 보입니다.
 제가 저장하고자 하는데이터는 서비스에서 핸들러로 구현된 스톱워치의 시간인데, MainActivity화면에서 다른 Activity로 화면이 넘어가도 계속 시간을 측정가능하게 해야합니다.  따라서 onStop이나 onPause에서 서비스를 해제하면 화면이 바뀔때 이 함수들이 호출이되므로 서비스가 정지되기때문에 onDestroy에 서비스 해제 코드를 구현 했던 것입니다.
그리고 제가 서비스 class안에는 onStartCommand에서 저장된 SharedPreference를 불러오고 onDestroy에서 SharedPreference를 이용한 시간 데이터를 저장하도록 하였습니다. 이렇게 하였는데도 강제종료시 Activity의 onDestory함수가 호출이 잘 되지 않아서 서비스가 정상 종료가 되지 않게 되고,  다시 앱을 실행해보면 저장된 객체가 마지막으로 안전하게 종료된 시점의  객체로 저장되어있습니다.
 즉, 앱을 종료했다 다시 켜도 종료하기 전의 측정된 시간이 그대로 남아있어야 하는 부분인데 이것이 정상종료가 되면 SharedPreference로 저장이 되는데, 강제종료가 되면 저장이 안되는 상황입니다. 답변으로 말씀해주신 onTaskRemove함수를 호출하는 방법은 4초이상이 걸리는 것 같아서 안되는 거 같고, SharedPreference는 제가 서비스 class안에 구현을 했지만 강제종료시 호출이 안되기때문에 계속 해결방법을 찾고있네요 ㅜㅜㅜㅜ  정말 좋은 답변 감사합니다.
그럼, 서비스는 onStop에서 종료하시지 마시고 대신 BraodCastReceiver나 binder를 이용해서 서비스로 부터 이벤트를 받아서 데이터를 저장해 보시죠. 액티비티에서는 onStart/onStop에 BroadCastReciver를 등록/해제해주면 될 것 같구요.
스톱워치의 시간을 매번 SharedPreference에 저장하면,
앱이 정지되었을 마지막 시간이 저장되는 거 아닌가요?
서비스의  Handler에서 1초 마다 호출되게 만들어서 남은 시간이나, 진행된 시간을 계속 저장하면, 언제 죽어도, 마지막 부분을 가져올 수 있죠.
종료된 시점을 정확하게 저장할 수 없다면, 중간중간 저장해서,
언제 죽더라도 1초던, 100ms 던, 최종 값을 계속 보관하면 문제가 해결 될 것 같네요.
예전에 자전거 앱을 만들어 계속 위치를 보관하게 했습니다.
앱이 중간에 죽을 수도 있기 때문에, 주기적으로 배열을 gson으로 저장했습니다. 많게는 1메가가 넘는 파일이지만, 앱이 강제로 죽더라도 최소한 1분 이상 저장데이터를 손해보지 않게 만든 거죠.
스톱 워치라면 저장할 내용이 많지 앟을테니, 100ms던 1초마다 저장해도 될 것 같네요.
이 방법과 onDestroy에서 저장하는 것을 두개 다 동시에 쓸 수 있죠.
@spark
BroadCastReceiver를 아직 사용해본적이 없어서 관련하여 좀더 공부해보고 이방법으로도 꼭 시도해봐야겠습니다. 정말 좋은 답변들 감사합니다!!!!
@Will Kim
진짜 감사합니다!ㅜㅜ 일단 이방법으로 해결을 했습니다!! Will Kim님 말씀대로 Handler내에서 SharedPreference를 사용하여 저장을 하면 정해진 시간마다 저장을 계속 할 수 있었네요..!!
아직도 공부할 부분들이 많은거 같습니다. 정말 큰 도움 되었습니다. 감사합니다~~!!!
...