[ProGuard] 안드로이드 애플리케이션의 최적화, 난독화 및 최소화

2023. 1. 14. 19:21모바일프로그래밍/안드로이드

728x90

Obfu.. 뭐라고?

뭐, 이 용어 외에도 이를 대표하는 여러가지 많은 기술 용어들이 존재 하지만, 이 용어가 과연 무엇을 뜻하는 것인지 모를 수 있다.

 

그래서 이것을 나만의 방식으로 안드로이드 애플리케이션의 사이즈를 줄여주고, 좀 더 빨리 실행될 수 있도록 효율화하고, 당신이 작성한 코드가 디컴파일러로 풀기 좀 더 어렵게 난독화 해 줄 수 있는 방법을 설명할 것이다.

 

우리는 이 ProGuard2 라고 불리는 이 자바 프로그램을 사용해서 적용한다면 코드를 빌드 할 때, 당신의 프로그램 코드가 이런 마술을 부릴 수 있을 것이다.

 

이런 마술을 부리기 위해서는 프로그램을Ant 스크립트를 사용해야 할 것이며, 정규 빌드 단계외의 추가 단계가 필요하다.

 

우리의 코드가 더 작아지고 더 빨라질 수 있을 것이다 라는 것이 이에 대한 짧은 답변이 될 것이다. 얼만큼 작아지고 빨리지는 지의 문제는 일반적으로 더 많이 작아지고 조금 빨라질 수 있는 곳을 찾아야 할 것이다. ProGuard가 할 수 있는 세가지 주요 기능이 여기에 있다. 아래 글의 많은 부분은 ProGuard웹사이트에서 인용 되었다. ​이 세가지 프로세스들을 설명하는 obfuscation라는 용어를 들어 보았을 것이다

 

사실, obfuscation이라는 것은 ProGuard 같은 프로그램이 할 수 있는 하나의 영역이다.

"shrink, obfuscate, and optimize" 라는 말 대신에, 우리는 이 블로그에서 언급하는 모든 세가지 영역에 대해서도 obfuscation라는 말로 대표해도 될 것이다.


1. Shrinking

자바소스코드(.java 파일들)은 일반적으로 바이트코드로(.class파일들) 컴파일 된다.

​바이트코드는 자바소스코드보다 좀 더 소형화 된다고 보지만, 여전히 사용되지 않는 많은 코드를 가지게 되는 데 특히 프로그램 라이브러리들을 포함하고 있을 때가 더욱 그렇다. ProGuard같은 Shrinking program 들은 바이트코드들을 해석해서 사용되지 않는 클래스들, 필드들 그리고 메서드들을 제거 할 수 있도록 분석한다. 이 프로그램은 예외 스택트레이스의 정보를 포함하여 기능적으로 똑같이 동작한다.

다음 현실적인 예제로서 다음 코드를 살펴보자: 

if (Config.LOGGING)
{
   TestClass test = new TestClass();
   Log.d(TAG, "[onCreate] testClass=" + test);
}
 

위의 코드는 개발 시에 일반적인 시나리오이다. 이는 당신의 코드를 테스트하고 디버깅하는 데 도움을 주기 위해서 생성한다. 최종 버전을 릴리즈 하기전에 Config.LOGGING을 false로할 것이며, 위의 코드는 실행되지 않을 것이다.

문제는 이코드가 여전이 당신의 애플리케이션 내에 남아 있다는 것이다.

 

이는 일을 크게 만들수도, 스누핑 해커가 절대 보지 말아야 할 코드를 가진 기본적인 보안 이슈 야기 할수 있다는 것이다. ​따라서, 이런 경우 코드를 줄여주는 것으로 이 문제를 아름답게 풀수 있다.

 

(Shrinking)위의 코드는 최종 제품에서는 완전히 제거되어 최종 패키지가 좀더 안전하며 작게 만들어 줄 수 있다.

 

2. Obfuscation

기본적으로, 컴파일된 바이트코드는 여전히 많은 디버깅 정보를 가지고 있다:

소스 파일 이름, 라인 넘버들, 필드 이름들, 메서드 이름들, 아규먼트 이름들, 변수 이름들 등등.

 

​이런 정보는 바이크코드를 디컴파일하면 리버스엔지니어가 전체 프로그램을 다 들여다 볼 수 있다는 것을 명확한 사실이 될 것이다. 때때로, 이것은 원치 않는 경우가 되어버린다.

 

ProGuard같은 난독화 도구들은 디버깅 정보를 제거 할수 있고 의미없는 문자열로 모든 이름을 교체하고, 리버스 엔지니어가 코드를 만드는 데 커다란 어려움을 겪게 만들어 준다.

 

그리고 코드를 소형화 한다는 것은 보너스가 된다. 프로그램은 클래스 이름들, 메서드 이름들 예외 스택트레이스에 걸릴수 있는 라인 넘버들을 제거하고는 기능적으로 똑같이 동작한다.

 


3. Optimizing

코드 축소(shrinking ) 단계에서 사용하지 않는 클래스, 필드들 그리고 메서드들 를 제거하는 것 이외에도 ProGuard는 또한 메소드 내부와 상호간 바이트 코드 레벨에서 최적화를 수행할 수 있다.

 

  • controlflow analysis(제어흐름분석),
  • data flow analysis(데이터흐름분석),
  • partial evaluation(부분 평가),
  • static single assignment,
  • global value numbering,
  • livenessanalysis

 

같은 고마운 기술을 바탕으로 ProGuard는 x * 2 를 x << 1 로 대체하는 것 같은 200가지 이상의 peephole 최적화 등을 수행할 수 있다.

이런 최적화의 긍정적인 영향은 코드가 수행되는 가상머신과 당신의 코드에 의존 적일 것이다. 이런 도구들은 단순 가상머신들에서 복잡한 JIT 컴파일러들을 가진 향상된 가상머신들 보다 더 좋은 이득을 볼 수 있을 것이다.

 

적어도 당신의 바이트코드는 좀 더 작아졌을 것이란 거다.

 


4. Ant 를 이용한 프로젝트 빌드

안드로이드 애플리케이션을 만들때, 혹은 빌드할 때, 많은 단계들이 진행된다.

​첫번째 자바 컴파일러는 소스 파일들을(i.e.텍스트.java 파일들) 자바 바이트 코드로(i.e. .class 파일들) 컴파일 해주며, ​안드로이드 SDK 는 자바 바이트 코드드를 Dalvik바이트코드로(i.e. .dex 파일들) 변환해 준다

 

마지막으로, 모든 리소스들과 코드들은 하나의 ZIP 파일 여기서는 .APK 파일이라고 불리는 파일로 묶여지게 된다.

 

ProGuard는 자바 바이트코드에 관련된 툴이기는 하지만, 우리는 빌드 단계에서 자바 바이트 코드가 달빅 바이트코드로 변환되기 전에, 자바 컴파일러에 의해서 생성된 클래스 파일들을 사용해서 ProGuard를 실행하기를 원한다.

따라서 우리가 아는 이클립스를 사용해서 안드로이드 패키지를 만드는 방법으로는 이 과정을 적용하기는 가능하지 않아 보인다(적어도, 내가 아는 한),그렇지만 Ant를 사용해서 애플리케이션을 빌드한다면 이야기는 달라진다. 내 블로그 포스트3를 참고하도록 한다. 아니면 이 블로그의 끝에 있는 샘플을 다운로드 받으면 끝이다.


5. Ant 빌드 스크립트에 ProGuard 추가하기

최신 버전의 ProGuard 배포버전을 다운로드4 한다. 그리고 그안의 라이브러리를 찾아서 내 프로젝트 디렉토리 어디든 알기 쉬운 위치에 옮겨놓는다.

 

​예를 들면 proguard/ 같은.. 예를들어 이 글을 쓸때 (4.5.1 배포버전) 최신버전의 배포버전파일 안에 lib/proguard.jar 파일을 내 소스 트리내의 proguard/proguard.jar로 복사를 해준다. 그리고 우리는 Ant 빌드 파일인 build.xml 파일에 다음을 추가해 주기로한다.

 

<!--================================================= -->
<!-- Obfuscation with ProGuard -->
<!-- ================================================= -->

<property name="proguard-dir" value="proguard"/>
<property name="unoptimized"value="${proguard-dir}/unoptimized.jar"/>
<property name="optimized"value="${proguard-dir}/optimized.jar"/>

<target name="optimize"unless="nooptimize">
<jar basedir="${out.classes.dir}" destfile="${unoptimized}"/>

<java jar="${proguard-dir}/proguard.jar"fork="true" failonerror="true">
<jvmarg value="-Dmaximum.inlined.code.length=16"/>
<arg value="@${proguard-dir}/config.txt"/>
<arg value="-injars ${unoptimized}"/>
<arg value="-outjars ${optimized}"/>
<arg value="-libraryjars ${android.jar}"/>
</java>

<!-- Delete source pre-optimized jar -->
<!--delete file="${unoptimized}"/-->

<!-- Unzip target optimization jar to original output, and deleteoptimized.jar -->
<deletedir="${out.classes.dir}"/>
<mkdir dir="${out.classes.dir}"/>
<unzip src="${proguard-dir}/optimized.jar"dest="${out.classes.dir}"/>

<!-- Delete optimized jar (now unzipped into bin directory) -->
<delete file="optimized.jar"/>

</target>
 

​optimize 라고 불리우는 Ant target을 자바 컴파일러와 dex 컴파일러 사이에 넣고, dex target을 다음과 같이 바꾼다:

 

Android 7 이나 그 이하 버전:

<!-- Converts this project's .classfiles into .dex files -->
<target name="-dex"depends="compile,optimize">
 

​Android 8 그 상위버전: -post-compile target을 주석처리하고 아래를 추가한다:

<targetname="-post-compile">
<antcall target="optimize"/>
</target>
 

6. ProGuard 환경설정

자 이제 ProGuard에게 나의 안드로이드 애플리케이션이 어떻게 동작해야 하는 지 지시하였다.

위의Ant 스크립트를 참조하는 proguard/config.txt 라는 파일을 생성한다.

Ant build 스크닙트를 통해 -libraryjars, -injars, 그리고 –outjars 옵션이 여기 대신에 전달 되겠지만,

다음은 ProGuard manual5 참조하였다.

-target 1.6
-optimizationpasses 2
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
-dump class_files.txt
-printseeds seeds.txt
-printusage unused.txt
-printmapping mapping.txt

# The -optimizations option disablessome arithmetic simplifications that Dalvik 1.0 and 1.5 can't handle.
-optimizations!code/simplification/arithmetic

-keep public class * extendsandroid.app.Activity
-keep public class * extends android.app.Application
-keep public class * extendsandroid.app.Service
-keep public class * extendsandroid.content.BroadcastReceiver
-keep public class * extendsandroid.content.ContentProvider

-keep public class * extends View {
public <init>(android.content.Context);
public<init>(android.content.Context, android.util.AttributeSet);
public<init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
}

# Also keep - Enumerations. Keep thespecial static
# methods that are required inenumeration classes.
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
 

-verbose and -printusageunused.txt 같은추가 출력물을 만들기 위해서 약간의 환경 설정을 더 추가해야 하는 것에 유의하자.

빌드 프로세스에서이런 어수선한 추가 결과물을 만들고 싶지 않다면 제거해도 된다 .

 

7. 결과

이제 준비는 끝났다!

 

커맨드라인에 ant release 명령을 실행하면, optimizer 실행되는 것을 볼 수 있을 것이다. config.logging 이 true 일때 아래 출력을 포함해서 테스트 프로젝트로 만들어 낸 출력이 진행 될 것이다:

>ant release
...
[java] Shrinking...
[java] Printing usage to [blog\obfuscation\proguard\unused.txt]...
[java] Removing unused program classes and class elements...
[java] Original number ofprogram classes: 8
[java] Final number of programclasses: 2
 

ProGuard를 -printusage unused.txt 옵션으로 환경설정을 하였기 때문에, 우리의 코드에서 무엇이 제거 되었는 지도 볼 수 있을 것이다:

 

com.androidengineer.obfu.Obfuscation:
private static final java.lang.String TAG
com.androidengineer.obfu.R
com.androidengineer.obfu.R$attr
com.androidengineer.obfu.R$drawable
com.androidengineer.obfu.R$layout
com.androidengineer.obfu.R$string
com.androidengineer.obfu.TestClass:
private static final java.lang.String TAG
 

정말 괜찮아 보이지 않는가?. 

proguard/unoptimized.jar 

andproguard/optimized.jar파일들을 살펴 볼 수도 있다.

 

상수들에 대한 플레이스홀더들로 단순히 쓰이는 많은 클래스가 제거되고 실제 문자 상수에 대한 레퍼런스들로 치환된로깅 코드에 사용된 TAG 문자변수가 제거되었다.

 

게다가, config.logging 프로퍼티를false로 두고 빌드를 하게 되면, 크기가 좀 더 많이 줄어 들어 있는 것을 확인 할 수 있다.

이 내용에 대한 최고는 우리의 모든 디버깅 코드를 제거한다는 것이다.

 

>ant release
...
[java] Original number ofprogram classes: 8
[java] Final number of programclasses: 1
 

클래스 하나가 더 제거 된 것을 눈여겨 보자- TestClass. 왜냐하면 Config.LOGGING이 true 일때만 이 클래스가 사용되기 때문에 obfuscation프로세스 동안 최종 빌드에서는 완전히 빠지게 되는 것이다.

 

그래서 당신의 소스코드에 원하는 만큼 디버깅코드를 집어 넣어도 이제 알아서 해주기 때문에 걱정하지 말라는 것이다.

물론, 왜냐하면 전형적인 안드로이드 애플리케이션이 아니기 때문에,

​우리의 단순한 테스트 프로젝트, 우리의 결과도 진짜 원하는 그것이 아닐 것이다.

 

proguard/unoptimized.jar 는 4,959 bytes이고 proguard/optimized.jar는 646 bytes이다.

 

하지만 내가 지금 작업하고 있는 애플리케이션은 천 개이상을 클래스에 대해서는 코드 크기가50% 축소를 이루어 냈다.

 

이 빌드업 프로세스는 그래서 세팅하는 데 공을 들여도 된다는 것이 내 의견이기도 하다.

 

8. ClassNotFoundExceptions

ProGuard로 난독화 작업을 한 애플리케이션을 실행 했을 때, 몇 경우에서 ClassNotFoundException이 발생하게 된다.

이 경우에는 ProGuard로 하여금 해당 클래스에 대해서 문제를 알려줄 수 있도록 config.txt 파일을 고쳐주도록 한다. 예를 들어,

# Keep classes which are not directlyreferenced by code, but referenced by layout files.
-keep,allowshrinking classcom.androidengineer.MyClass
{
*** (...);
}
 

이 시나리오의 경우 일어나는 경우가 거의 드물다.

MyButton extendsButton 같은안드로이드 레이아웃 파일 내의 안드로이드 View 클래스의 자식을 참조 한 경우가 그런 경우가 될 수있다.

그렇지만 일반적인 코드에서는 이 클래스가 레퍼런스 되지 않는다.​ProGuard documentation6에서 좀 더 정보를 얻을 수 있을 것이다.

 

9. Android SDK 7 버전 이상에 대한 업데이트

​구글은 향후 SDK버전내에 Ant 스크립트들을 업데이트 하였다.

​$[android-jar}를 ${android.jar}으로 이름을 변경한 것이다. 이 때문에 빌드가 멈춘 이유가 된다.

이에 대한 솔루션은 이 프로퍼티가 존재하지 않으면 정의해 주는 것이다:

<!-- In newer platforms, the build setuptasks were updated to begin using ${android.jar} instead of${android-jar}. This makes them bothcompatible. -->
<target name="target-new-vars"unless="android-jar">
<propertyname="android-jar" value="${android.jar}"/>
</target>

<!-- Be sure to call target-new-varsbefore anything else. -->
<target name="config"depends="target-new-vars,clean">
 

아래 샘플 프로젝트는 업데이트 되었다

 

10. Android SDK 8 버전 이상에 대한 업데이트

참, 구글은 ant 빌드 파일을 다시 바꾸었다. 이번에는 쉽게 짜맞출 수 있도록 만들었는 데,build.xml 파일이 이번에 더 작아졌다. 새로운 멋진 섹션이 추가되었다:

<!-- extension targets. Uncommentthe ones where you want to
do custom work in between standard targets -->
<!--
<target name="-pre-build">
</target>
<target name="-pre-compile">
</target>

[This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override
${out.dex.input.absolute.dir}]
<target name="-post-compile">
</target>
-->
 

우리가 해야 할 일이라곤 -post-compile Ant 타켓을 주석처리하고 우리의 obfuscation Ant 타겟을 추가하는 것이다.

<targetname="-post-compile">
<antcall target="optimize"/>
</target>
 

Build 파일의 완전본은 여기7에있다.

 

11. 구글의 License Verification Library (사용하기

구글 라이선스 서비스를 사용하는 라이브러리 License Verification Library 위해서, 추가라이브러리 내에 추가 클래스가 난독화 되어지는 방지해야 한다.

다음 라인이 proguard/config.txt 내에 존재하는 지 확인하자 .

-keep classcom.android.vending.licensing.ILicensingService
 

12. 샘플 애플리케이션

샘플 애플리케이선은 단순한 Hello world 애플리케이션이지만 이 튜토리얼에서 설명한 커스텀 빌드 스크립트와 ProGuard 라이브러리를 포함한다.

 

첫째, 커맨드 창을 열고 프로젝트 디렉토리에서 "android update project -p ." 명령을 사용해야만 한다.

 

이때 local.properties 파일 내에는 SDK 경로가 명시되어야 툴을 세팅할 수 있다.

​그런다음. build.properties내의 config.logging의 값을 변경하면서, 로깅을껏다 켰다 할 수 있다.

 

마지막으로, obfuscated 되어지고 서명된.apk 파일을 생성 하기 위해서 ant release를 수행해서 애플리케이션을 빌드 한다.

 

빌드 할 때 문제가 있다면 앞선 블로그 포스팅8을 참고 해서 Ant 빌드 환경을 제대로 잡아야 할 것이다.

 

프로젝트 소스코드obfuscation.zip9 (600 Kb)

위 Android API level 8에 대한 빌드 파일: build.xml (4.52 Kb)

 

결론을 말하자면...

글쎄? 난독화에 대한 부분은 그렇게 큰 의미가 있을까 하는 데 최적화 부분이랑 코드 최소화 부분은 잘만 된다면 꽤 괜찮을 듯...

 

 

이상.


 

728x90