글쓴이 보관물: mcsong

mcsong

mcsong에 대하여

Software Engineer, 카산드라 완벽 가이드(http://www.yes24.com/24/goods/5847220) 번역

앱(APK) 크기를 줄이는 또 다른 방법

앱 크기를 줄이는 팁으로 사용하지 않는 이미지, Proguard 사용 등의 여러 가지 방법이 있다. 이 방법들 모두는 결국 zip 포맷으로 패키징되는 파일을 개수나 크기를 줄여서 .apk 파일의 크기를 줄이는 것이다. 이 글에서는 .apk 파일이 포함하는 또 다른 파일인 텍스트 파일을 제거해서 앱의 크기를 줄이는 방법을 살펴보자. 텍스트 파일이라서 앱 크기에 큰 영향을 미치지는 않지만, 그래도 약간의 크기를 줄일 수 있는 또 다른 방법으로 알아두면 좋겠다.

이 방법은 안드로이드 스튜디오에서 빌드와 패키징을 담당하는 툴인 그레들의 빌드파일(build.gradle)에 packagingOptions{} 영역에 패키징시에 제외할 파일을 기술하는 것이다. 이 설정은 아래와 같이 동일 파일의 중복으로 인한 에러를 해결하는데 또한 사용한다.

Error:Execution failed for task ':app:transformResourcesWithMergeJavaResForDebug'.
> com.android.build.api.transform.TransformException: com.android.builder.packaging.DuplicateFileException: Duplicate files copied in APK META-INF/DEPENDENCIES

그래서, 위 에러를 해결하기 위한 최소의 설정은 다음과 같다.

android {
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
    }
}

컴파일한 앱의 크기를 확인해 보면, 크기가 9,154,124 바이트이다.

다음으로 어느 파일을 제외할 수 있는지 살펴보자. 안드로이드 스튜디오에서 Build > Analyze Apk 로 위의 파일을 선택하면 아래의 화면을 볼 수 있다.

위 화면에서 체크한 파일들을 앱 패키징시에 제외시키도록 packageOptions에 추가하자. 그리고 일반적으로 많이 사용하는 텍스트 파일도 역시 미리 포함시켜 넣어서 아래와 같이 설정한다.

android {
	packagingOptions {
	    exclude 'META-INF/DEPENDENCIES.txt'
	    exclude 'META-INF/DEPENDENCIES'
	    exclude 'META-INF/dependencies.txt'
	    exclude 'META-INF/LICENSE.txt'
	    exclude 'META-INF/LICENSE'
	    exclude 'META-INF/license.txt'
	    exclude 'META-INF/LGPL2.1'
	    exclude 'META-INF/ASL2.0'
	    exclude 'META-INF/NOTICE.txt'
	    exclude 'META-INF/NOTICE'
	    exclude 'META-INF/notice.txt'
	    exclude 'META-INF/MANIFEST.MF'
	    exclude 'META-INF/manifest.mf'
	    exclude 'META-INF/MANIFEST'
	    exclude 'META-INF/manifest'

	    exclude 'META-INF/CHANGES'
	    exclude 'META-INF/README'
	    exclude 'META-INF/NOTES.TXT'

	    exclude 'licenses/thoughtworks.TXT'
	    exclude 'licenses/extreme.indiana.edu.license.TXT'
	    exclude 'licenses/javolution.license.TXT'
	}
}

이제 다시 패키징한 파일을 살펴보자. 필요 없는 텍스트 파일들을 제외하고, 컴파일한 파일의 크기는 9,137,085 바이트이다.

이 과정으로 앱의 크기가 17,000 바이트를 줄일 수 있다. 이 과정으로 메가 바이트 단위로 앱의 크기를 줄이기는 힘들겠지만, 약간이나마 크기를 줄이는데 기여할 수 있다.

멀티 dex 사용하기

안드로이드(Android) 앱(APK) 파일은 DEX(Dalvik Executable) 파일 형식의 실행 가능 바이트코드 파일을 포함하고 있다. 개별 .dex파일은 사용할 수 있는 메서드의 총 개수가 65,536로 제한되어 있다. 그래서 이 제한을 보통 ’64K 참조 제한’이라고 한다.

안드로이드 스튜디오의 메뉴에서 Build > Analyze APK… 를 선택하면 .apk 파일을 구성하는 것들을 자세히 살펴볼 수 있고, 이 제한을 넘어서 더 많은 메서드등을 사용하는데는 2개 이상의 .dex 파일이 필요하다. 그래서 2개 이상의 .dex 파일을 생성하도록 앱 빌드 프로세스를 구성해야 하며, 이것을 multidex 구성이라고 한다. 아래 이미지를 보면, classes.dex와 classes2.dex파일로 .dex 파일이 2개 있는 것을 알 수 있다. 

안드로이드 앱에서 멀티 dex를 사용하는 방법은 3가지의 형태가 있을 수 있다. 첫번째가 Application 클래스를 사용하지 않는 경우, 두번째는 Application 클래스를 사용하는 경우, 그리고 마지막은 MultiDexApplication 클래스를 사용하는 경우이다.

멀티 dex를 사용하는데 필요한 과정의 기본은 build.gradle 파일에 multidex 라이브러리를 추가하고, multidexEnabled 옵션을 true로 설정하는 것이다.

android {
	defaultConfig {
		multiDexEnabled true
	}
}

dependencies {
	compile 'com.android.support:multidex:1.0.1'
}

이제 위 설정이 완료됐으면, 멀티 dex를 사용하는 3가지 형태에 대해서 살펴보자.

1. Application을 사용하지 않는 경우

이 경우에는 아래의 코드를 AndroidManifest.xml 파일에 아래처럼 Application을 설정해야 한다.



    
        ...
    

2. Application을 사용하는 경우

프로젝트의 AppApplication 클래스가 Application을 상속해서 구현하고 있다고 가정한다. 이 경우에는 아래와 같은 코드가 AndroidManifest.xml 파일에 설정해야 한다.



    
        ...
    

그리고, AppApplication 에 아래의 코드를 추가한다.

@Override
protected void attachBaseContext(Context base) {
	super.attachBaseContext(base);
	MultiDex.install(this);
}

3. MultiDexApplication을 사용하는 경우

이 경우가 가장 간단하게 사용할 수 있다. AndroidManifest.xml은 위 2.와 동일하게 설정한다. 그리고, 아래처럼 Application 클래스가 MultiDexApplication을 상속하면 된다.

public class AppApplication extends MultiDexApplication {
}

어떻게 MultidexApplication만 상속하면 돼는지, 이 클래스의 소스를 살펴보면 다음과 같다.

package android.support.multidex;

import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;

public class MultiDexApplication extends Application {
	public MultiDexApplication() { 
	}

	protected void attachBaseContext(Context base) {
		super.attachBaseContext(base);
		MultiDex.install(this);
	}
}

이 소스를 보면, Application에서 사용하는 코드를 제공하는 래퍼 클래스임을 알 수 있다. 결론으로, Application 클래스를 사용하는 것은 꽤 유용하므로, 개인적으로 3번째 방법을 사용해서 멀티 dex를 제공하는 것이 좋겠다.

안드로이드 7.0 AsyncTask 개선점

안드로이드 7.0 누가(NOUGAT)에서 AsyncTask의 개선 점을 살펴보자. 안드로이드 6.0 AsyncTask 소스와 7.0 소스의 차이를 확인(Diff)해 보면, 아래 화면에서 보는 2가지 정도의 차이가 있다. 차이를 살펴보면 다음과 같다.

1. 작업을 처리하는 스레드의 상수 수정.

코드를 살펴보면, CORE_POOL_SIZE와 KEEP_ALIVE의 값이 수정된 것을 확인할 수 있다. CORE_POOL_SIZE가 주석에서 보다시피 2~4개 유지하게 변경했고, 작업 처리 스레드의 종료시기를 늦추는 것을 알 수 있다.

2. ThreadPoolExecutor 수정.

ThreadPoolExecutor는 작업을 처리하는 여러 스레드를 유지하고, 작업을 큐로 유지하는 디스패처이다. 아래는 위에서 변경한 상수로 ThreadPoolExecutor를 생성하는 예제로, 작업이 추가되지 않는다면 30초 뒤에 스레드 풀의 스레드를 종료시킨다.

버터나이프(ButterKnife)를 사용한 효율적인 추상화

대부분 프로젝트에서 소스의 양은 복잡도와 밀접한 관계가 있다. 그래서 OOP 언어에서는 공통 로직의 추상화와 모듈화로 소스의 복잡도를 줄이는 방향으로 가이드하고 있다는 것을 알 수 있다. 자바언어는 도메인에 특화된 여러 프레임웍이 제공하는 의존성 주입(DI)을 사용해서 코드의 복잡도를 줄이고 있다.

안드로이드 앱 프로젝트 또한 프레임웍 기반에서 개발한다. 그래서 안드로이드 프로젝트를 지원하는 각종 라이브러리가 존재하지만 프레임웍은 존재하지 않는 이유이기도 하다. 그래서 대부분의 의존성을 주입하는 라이브러리들은 어노테이션으로 컴파일 타임에 코드를 생성하는 형태이다. 이 형태는 런타임에 의존성 주입을 하는 형태보다 성능상의 이점이 있어서 표준처럼 사용하고 있다.

의존성 주입을 지원하는 대표 라이브러리로 버터나이프(ButterKnife)와 대거(Dagger)가 있다. 개인적으로도 버터나이프를 사용하고 있고, 안드로이드 프로젝트를 시작할 때에 처음 사용하는 라이브러리이다. 그래서 이 라이브러리를 사용해서, 비교적 좋은 형태의 추상화 예제를 살펴보겠다.

아래 예제는 안드로이드 프로젝트에서의 가장 많은 템플릿(Boilerplate) 코드로 뷰(View)에서 위젯(Widget)을 로드하는 예제이다. 아래 예제에서 액티비티(Activity)와 프레그먼트(Fragment)에서 어떻게 사용하고, 개선하는지 살펴보자.

1. 안드로이드 기본 예제

아래는 안드로이드 앱에서 많이 사용하는 액티비티와 프레그먼트의 예제이다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */
public class MainActivity extends AppCompatActivity {

	private TextView tv01;
	private TextView tv02;
	private TextView tv03;
	private TextView tv04;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		tv01 = (TextView) findViewById(R.id.activity_main_tv_01);
		tv02 = (TextView) findViewById(R.id.activity_main_tv_02);
		tv03 = (TextView) findViewById(R.id.activity_main_tv_03);
		tv04 = (TextView) findViewById(R.id.activity_main_tv_04);
	}
}

액티비티 예제

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public class MainFragment extends Fragment {

	private TextView tv01;
	private TextView tv02;
	private TextView tv03;
	private TextView tv04;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.fragment_main, parent, false);

		tv01 = (TextView)view.findViewById(R.id.fragment_exview_tv_01);
		tv02 = (TextView)view.findViewById(R.id.fragment_exview_tv_02);
		tv03 = (TextView)view.findViewById(R.id.fragment_exview_tv_03);
		tv04 = (TextView)view.findViewById(R.id.fragment_exview_tv_04);

		return view;
	}

}

프레그먼트 예제

위 예제들은 findViewId() 메서드를 사용해서 화면에 배치한 위젯을 로드하는 예제이다. 이 형태로 화면에서 많은 위젯을 로드하면, 코드의 양도 그만큼 늘어난다.

2. 버터나이프(Butterknife) 예제

버터나이프를 사용해서 액티비티나 프레그먼트에서 화면을 로드하는 예제이다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import net.sjava.example.abstraction.R;

import butterknife.BindView;
import butterknife.ButterKnife;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public class ButterknifeActivity extends AppCompatActivity {

	@BindView(R.id.activity_main_tv_01) TextView tv01;
	@BindView(R.id.activity_main_tv_02) TextView tv02;
	@BindView(R.id.activity_main_tv_03) TextView tv03;
	@BindView(R.id.activity_main_tv_04) TextView tv04;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		ButterKnife.bind(this);
	}
}

액티비티 예제

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import net.sjava.example.abstraction.R;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public class ButterknifeFragment extends Fragment {
	@BindView(R.id.fragment_exview_tv_01) TextView tv01;
	@BindView(R.id.fragment_exview_tv_02) TextView tv02;
	@BindView(R.id.fragment_exview_tv_03) TextView tv03;
	@BindView(R.id.fragment_exview_tv_04) TextView tv04;

	private Unbinder unbinder;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.fragment_main, parent, false);
		unbinder = ButterKnife.bind(this, view);
		return view;
	}

	@Override
	public void onDestroyView() {
		super.onDestroyView();
		unbinder.unbind();
	}
}

프레그먼트 예제

버터나이프 예제들은 @BindView 어노테이션을 사용해서 화면의 위젯을 로드한다. 이 예로 위의 안드로이드 기본 예제보다 약간의 수고를 덜 수 있다.

3. 버터나이프 추상화 예제

버터나이프를 사용해서 액티비티와 프레그먼트에서 추상화하는 예제와 추상화한 액티비티와 프레그먼트를 사용하는 예를 살펴보자.

3.1 추상화 예제

import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.v7.app.AppCompatActivity;

import butterknife.ButterKnife;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */
public abstract class AbsBaseActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		super.setContentView(getLayoutId());
		ButterKnife.bind(this);
	}

	@LayoutRes
	public abstract int getLayoutId();
}

추상화 액티비티 예제

이 추상화한 클래스는 abstract 클래스로 이 클래스를 상속하는 클래스는 getLayoutId() 메서드를 구현하게 만든다. 이 메서드에서는 액티비티의 화면인 xml 파일의 id를 반환하도록 한다.


import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import butterknife.ButterKnife;
import butterknife.Unbinder;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public abstract class AbsBaseFragment extends Fragment {

	Unbinder mUnbinder;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		View view = inflater.inflate(getLayoutId(), container, false);
		mUnbinder = ButterKnife.bind(this, view);
		return view;
	}

	@LayoutRes
	public abstract int getLayoutId();

	@Override
	public void onDestroyView() {
		super.onDestroyView();
		mUnbinder.unbind();
	}
}

추상화 프레그먼트 예제

이 추상화한 프레그먼트는 추상화한 액티비티의 getLayoutId()와 더불어, onDestroyView() 메서드에서 버터나이프로 읽어들인 뷰를 제거하는 메서드를 추가했다.

3.2 사용 예제

import android.os.Bundle;
import android.widget.TextView;
import net.sjava.example.abstraction.R;

import butterknife.BindView;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public class ButterknifeMainActivity extends AbsBaseActivity {

	@BindView(R.id.activity_main_tv_01) TextView tv01;
	@BindView(R.id.activity_main_tv_02) TextView tv02;
	@BindView(R.id.activity_main_tv_03) TextView tv03;
	@BindView(R.id.activity_main_tv_04) TextView tv04;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	}

	@Override
	public int getLayoutId() {
		return R.layout.activity_main;
	}
}

액티비티 예제

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import net.sjava.example.abstraction.R;

import butterknife.BindView;

/**
 * Created by mcsong@gmail.com on 2016-11-14.
 */

public class ButterknifeMainFragment extends Fragment {
	@BindView(R.id.fragment_exview_tv_01) TextView tv01;
	@BindView(R.id.fragment_exview_tv_02) TextView tv02;
	@BindView(R.id.fragment_exview_tv_03) TextView tv03;
	@BindView(R.id.fragment_exview_tv_04) TextView tv04;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
		return super.onCreateView(inflater, parent, savedInstanceState);
	}
}

프레그먼트 예제

* 예제 코드 : https://github.com/mcsong/AbstractionExample
이 글에서 버터나이프를 사용해서 비교적 효율적으로 추상화해서 사용하는 방법을 살펴봤다.

안드로이드 프로젝트 빌드 유지(CI)를 위한 팀시티(TeamCity) 설정(2/2)

이 글은 안드로이드 프로젝트의 빌드를 유지하는 세 개의 글 중에 세 번째이다.

이 글은 팀시티가 저장소의 소스를 내려받고, 안드로이드 프로젝트 빌드를 완료하면, 등록된 계정에게 알려주도록 알람을 설정하는 내용이다. 이 글에서는 이메일과 메신저(Jabber 기반) 계정으로 알림을 송/수신 하는 설정, 빌드 파일(apk)을 다운로드 받게 설정, 그리고 수신하는 내용(템플릿)을 수정하는 방법에 대해서 살펴보자.

1. 알람 발송 계정 설정

구글의 서비스를 사용한다고 가정하고, 구글의 이메일과 메신저로 알림을 받도록 설정해보자.
구글 메일과 메신저 서비스(GTalk)는 아래 링크의 설정을 참고하면 된다.
https://confluence.jetbrains.com/display/TCD10/Setting+up+Google+Mail+and+Google+Talk+as+Notification+Servers
설정 메뉴는 Administration > Server Administration > Email, Jabber Notifier 메뉴에서 확인할 수 있다.
– 이메일 설정 화면
setting_noti_jabber

– 메신저(Jabber) 설정 화면
setting_noti_gmail

이제 알람을 발송할 계정 설정을 완료했다.

2. 알람 수신 계정 설정

다음으로, 알람을 받는 계정의 Email과 Jabber 계정을 설정해 보자. Administration > Users 에서 알람을 받을 계정을 선택한다. 우측의 “Watched Builds and Notifications” 에 Email과 Jabber Notifier를 선택한다. 그리고, Add New Rule을 선택하면 아래와 같은 화면을 볼 수 있다. 이 화면에서 알람을 받을 프로젝트의 빌드를 선택하고, 빌드의 상태인 “Build fails”과 “Build is successful” 을 선택하고 저장한다.
setting_jabber setting_email

이제 Email과 Jabber로 알람을 받을 준비가 완료됐다.

3. 빌드 파일 설정

빌드한 apk 파일을 다운로드 할 수 있게 산출물(Artifact)로 설정해야 한다. 아래 설정은 Projects > Build > General Settings에 아래와 같은 설정이 있고, Artifact paths에 apk 파일을 설정한다. 이제 빌드한 파일을 배포할 준비가 되었다.
apk_path_settings

4. 수신 내용 변경

이제 위에서 설정한 이메일과 메신저를 사용해서 소스 빌드에 대한 이벤트(성공/실패)를 수신 받게 된다. 기본적으로 수신받는 화면은 아래와 같다.
jabber_notification_alert email_notification_alert

위에서 보는 이메일이나 메신저의 알림 내용을 추가해서 조금 더 편리하게 알림을 사용할 수 있다. 아래에서 빌드된 apk 파일을 내려받기 쉽게 링크를 추가해 보자. 팀시티의 알림 템플릿은 아래의 위치에서 확인할 수 있고, 알림의 형태에 따라서 다음과 같이 나눠져 있다.

C:\TeamCityData\config\_notifications\email\build_successful.ftl
C:\TeamCityData\config\_notifications\jabber\build_successful.ftl

위에서 email 폴더에 있는 build_successful.ftl 파일을 수정해 보자. 우선 프로젝트의 빌드 파일의 위치를 확인해 보면 다음과 같다.

<#-- MODIFICATION START -->
  <#if buildType.externalId = "Nbarcode">    
    Click here to download APK.
      
  </#if>
<#-- MODIFICATION END -->

팀시티의 10.x 버전의 다운로드 위치는 위의 URI 형태를 띄고 있다. 이제 이 내용을 템플릿 파일의 적절한 위치에 복사를 하고 저장한다.

  <@common.build_comment build/>
  
  <#-- MODIFICATION START -->
  <#if buildType.externalId = "Nbarcode">
    
    Click here to download APK.
    
  </#if>
<#-- MODIFICATION END -->
  
  <@common.build_changes var.changesBean/>

이제 저장하고 소스를 커밋하면 아래와 같이 이메일과 메신저로 알람을 받게 된다.
email_notification_alert jabber_notification_alert

* Reference
– TeamCity에서 안드로이드앱 배포하기