안드로이드 4.4 킷캣 1개월만에 1.1% 점유율, 갈길이 멀다.



안드로이드 4.4 킷캣 발표 1개월지난 시점에 1.1%의 점유율을 차지했습니다. 애플의 iOS 7 발표후 2일만에 31.7%, 한달만에 51.8%로 업데이트된것에 비해 대조적이다.

Android Dashboards

젤리빈의 점유율은 절반이상 차지하여 가장많은 점유율을 보였으며, 그다음으로 진저브레드가 25% 점유율을 보였다. 개발자가 가장고민중 하나가 진저브레드를 지원해야 하는지 말아야하는지가 가장 고민 일텐데, 1/4이면 아직도 무시 못할것 같다. 허니컴 이상 개발하는 것과 진저브레드를 지원하게 하는것은 손이 많이 가는 작업이다. SupprotLibrary를 쓴다던가 SDK버전을 분리해서 기능을 뺀거나 다른 방법으로 기능을 구현한다 던가 작업이 다른 버전에 비해 많이 필요하다. 

그리고 이렇게 점유율이 나오지 않는다면 최신의 OS가 나오더라도 사용유저가 없기때문에 최신 SDK를 써서 새로운 기능을 개발 할 이유도 없어진다고 생각한다.

진저브레드의 점유율 예측 해보면 1개월에 1.5%정도씩 감소 한다고 봤을때 약 1년은 지나야 10%때의 점유율로 떨어 지지 않을까 생각된다.  

iOS처럼 즉각적으로 업데이트가 이루어 지지 않는것은 구글에서 업데이트가 이루어 지는게 아니라 제조사에서 각 디바이스별로 업데이트를 해야 하는 구조 이기때문에 이렇게 파편화가 더 심해 지는것에 한 몫 하는것 같다. 이런문제점은 구글과 제조사의 협업이 없는한 지속적으로 발생 될 것으로 보인다.




신규 안드로이드 앱 최소 지원 OS버전 선택은?

안드로이드는 다양한 기기의 파편화 뿐만 하니라 OS에서도 파편화가 아주 심각 하다. 애플의 iPhone의 경우 얼마전 iOS7 정식 발표 첫날 29%의 사용자가 업데이트를 했다. 이와는 대조적으로 안드로이드는 파편화가 아주 심각하다. 이런 점에서 볼때 개발자는 신규 OS가 나왔음에도 불구하고 하위호환성을 위해서 최신 OS의 기술을 사용하기가 참 애매하다. 얼마전 안드로이드 4.4(킷캣)이 발표 되었지만 여기에 들어 있는 SDK를 가지고 뭔가의 기능을 구현하는 앱을 찾아보기란 쉽지 않다. 제조사별 다양한 기기를 일일이 OS에 맞는 롬을 만들어야 하기에 그만큼 업데이트도 더디기 때문에 iOS와 같이 즉각적인 업데이트는 기대하기 힘들다. 즉 이런 다양한 문제점으로 인해 파편화는 점점 더 심해지고 있는 시점에서 우리 안드로이드 개발자는 OS파편화에 대해서 어떻게 대응 해야 할까 생각 해보게된다.

신규 앱 개발시 최소지원 OS 선택은?

신규 앱개발시 가장 고민거리중 최소 OS선택 기준점이 딱하나 있다. 안드로이드 2.3.3(진저브레드)을 지원할지 말지의 문제다. 아래 직접 운영중인 앱의 구글통계를 보면 2.3.3(진저브레드)가 전체의 25%정도를 차지한다.(다른앱도 비슷할것으로 추정) 즉, 안드로이드 4.0(ICS)로 최소지원버전으로 잡게 되면 전체의 30%를 버리게 된다는 것이다. 이 통계를 이때까지 앱을 설치한 사용자들의 통계이다. 즉 신규앱을 개발해서 서비스할 경우 안드로이드 4.0(ICS)이하가 설치하는 비중이 30%라는 것은 아니다.  

그러면 신규 사용자의 비율을 보기위해 일일 설치 사용자수를 확인 해볼 필요가 있다. 아래 화면을 최근 3개월 동안 설치한 사용자들의 OS 데이터들이다. 안드로이드 4.0(ICS)이하의 버전은 7%정도뿐이다. 위의 현재 설치된 데이터 30%와 사뭇 다르다. 

그럼 왜 이런 데이터들이 나타나는 것일까?

앱을 설치한 폰을 중고로 팔아 버리거나 초기화 하는 경우 데이터는 누적되기 때문이 이런 차이점을 보이게 되는것으로 생각된다. 그렇기 때문에 일일기기 설치 또는 업데이트 수를 보고 판단하면 될것이다. 

앱 업데이트시 지원 OS변경은?

업데이트 앱의 OS 지원은 아주 신중해야 한다. OS버전이 낮다는 이유로 잘쓰던 앱이 업데이트가 되지 않는 다면 분명히 문제점이 있다. Support Library를 써서라도 유지를 해야한다. 

즉, 결론을 내린다면 현 시점에서 신규 앱을 개발하게 된다면 통계상 안드로이드 4.0이하 OS 점유율이 15% 정도로 봤을때 4.0 이상의 SDK를 반드시 필요로 하는 앱이라면 안드로이드 4.0(ICS)를 최소 지원버전으로 잡아도 그렇게 무리는 없을듯 생각된다. 

구글에서도 이런 OS파편화의 문제점으로 Supprot Library(ActionBar, Fragment, RS등)를 제공해주기 때문에 개발상의 어려움이 없다면 하위 호환성을 위해 사용하는 것도 나쁘진 않다.

Android OS별 기능 분기

안드로이드 앱을 개발 하다보면 특정 API 부터 적용되는 기능들이 있다. 상위버전은 지원하게해야 되고 하위버전은 지원하지 않기 위해 보통은 API Level을 보고 판단하게 된다. 제조사가 OS의 기능을 100% 보장한다면 문제가 없지만 꼭 몇몇 제조사들은 빼버리기 때문에 ClassNotFoundException 예외가 발생한다. 

이런 문제점을 해결 하기위해 API Level 체크 외 다른 방법을 소개 할까한다.

기존 체크 방식
private void init() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH){
        //사용가능한 버전이 아님
    }
    else{
        //사용가능한 버전
    }
}

 

위의 코드처럼 대부분 현재 SDK버전과 지원가능한 기능의 SDK버전을 비교해서 처리 하게 된다. 이럴 경우 실제로 제조사에서 해당기능을 임의로 빼버리는 경우 문제가 생기게 된다. 단순히 Try{} Catch{}로 Exception처리 할 수도 있으나, 좀 더 좋은 방법을 알아 보자.

ClassLoader와 Method라는 Class를 이용해서 Class와 Method를 invoke하는 방식이다.

좀 더 좋은 방법
public class MetadataEditorCompat {

    private Method mPutStringMethod;
    private Method mPutBitmapMethod;
    private Method mPutLongMethod;
    private Method mClearMethod;
    private Method mApplyMethod;

    private Object mActualMetadataEditor;

    /**
     * The metadata key for the content artwork / album art.
     */
    public final static int METADATA_KEY_ARTWORK = 100;

    private MetadataEditorCompat(Object actualMetadataEditor) {
        if (sHasRemoteControlAPIs && actualMetadataEditor == null) {
            throw new IllegalArgumentException("Remote Control API's exist, " + "should not be given a null MetadataEditor");
        }
        if (sHasRemoteControlAPIs) {
            Class metadataEditorClass = actualMetadataEditor.getClass();

            try {
                mPutStringMethod = metadataEditorClass.getMethod("putString", int.class, String.class);
                mPutBitmapMethod = metadataEditorClass.getMethod("putBitmap", int.class, Bitmap.class);
                mPutLongMethod = metadataEditorClass.getMethod("putLong", int.class, long.class);
                mClearMethod = metadataEditorClass.getMethod("clear", new Class[] {});
                mApplyMethod = metadataEditorClass.getMethod("apply", new Class[] {});
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        mActualMetadataEditor = actualMetadataEditor;
    }

    public MetadataEditorCompat putString(int key, String value) {
        if (sHasRemoteControlAPIs) {
            try {
                mPutStringMethod.invoke(mActualMetadataEditor, key, value);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return this;
    }

    public MetadataEditorCompat putBitmap(int key, Bitmap bitmap) {
        if (sHasRemoteControlAPIs) {
            try {
                mPutBitmapMethod.invoke(mActualMetadataEditor, key, bitmap);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return this;
    }

    public MetadataEditorCompat putLong(int key, long value) {
        if (sHasRemoteControlAPIs) {
            try {
                mPutLongMethod.invoke(mActualMetadataEditor, key, value);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return this;
    }

    public void clear() {
        if (sHasRemoteControlAPIs) {
            try {
                mClearMethod.invoke(mActualMetadataEditor, (Object[]) null);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }

    public void apply() {
        if (sHasRemoteControlAPIs) {
            try {
                mApplyMethod.invoke(mActualMetadataEditor, (Object[]) null);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }
}

위 예제코드는 RemoteControlClient로 ICS부터 사용가능 하다. getClassLoader()를 통해 Class를 가져오고, getMethod()를 통해 해당 Method를 가져와 invoke하는 방식이다. getClassLoader(), getMethod()로 호출시 ClassNotFoundExecpotion, NoSuchMethodException이 발생 하면 해당 클래스와 메소드를 사용하지 않도록 처리한다. 

실제 구글에서 안드로이드 RandomMusicPlayer 셈플 코드를 보면 이처럼 구성이 되어있다. 이렇게 method invoke방식이 약간 복잡 할 수 있으나 제조사의 안드로이드 OS의 커스터마이징시 발생 할 수 있는 예외를 처리 할 수 있다.