Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> App的打磨之路(中)

App的打磨之路(中)

編輯:關於Android編程

一、混淆

1、混淆原理

Java 是一種跨平台的、解釋型語言,Java 源代碼編譯成中間”字節碼”存儲於 class 文件中。由於跨平台的需要,Java 字節碼中包括了很多源代碼信息,如變量名、方法名,並且通過這些名稱來訪問變量和方法,這些符號帶有許多語義信息,很容易被反編譯成 Java 源代碼。為了防止這種現象,我們可以使用 Java 混淆器對 Java 字節碼進行混淆。
混淆就是對發布出去的程序進行重新組織和處理,使得處理後的代碼與處理前代碼完成相同的功能,而混淆後的代碼很難被反編譯,即使反編譯成功也很難得出程序的真正語義。被混淆過的程序代碼,仍然遵照原來的檔案格式和指令集,執行結果也與混淆前一樣,只是混淆器將代碼中的所有變量、函數、類的名稱變為簡短的英文字母代號,在缺乏相應的函數名和程序注釋的況下,即使被反編譯,也將難以閱讀。同時混淆是不可逆的,在混淆的過程中一些不影響正常運行的信息將永久丟失,這些信息的丟失使程序變得更加難以理解。

2、混淆語法
-include {filename}    從給定的文件中讀取配置參數
-basedirectory {directoryname}    指定基礎目錄為以後相對的檔案名稱
-injars {class_path}    指定要處理的應用程序jar,war,ear和目錄
-outjars {class_path}    指定處理完後要輸出的jar,war,ear和目錄的名稱
-libraryjars {classpath}    指定要處理的應用程序jar,war,ear和目錄所需要的程序庫文件
-dontskipnonpubliclibraryclasses    指定不去忽略非公共的庫類
-dontskipnonpubliclibraryclassmembers    指定不去忽略包可見的庫類的成員

保留選項
-keep {Modifier} {class_specification}    保護指定的類文件和類的成員
-keepnames {class_specification}    保護指定的類和類的成員的名稱(如果他們不會在壓縮步驟中刪除)
-keepclassmembers {modifier} {class_specification}    保護指定類的成員,如果此類受到保護他們會保護的更好
-keepclassmembernames {class_specification}    保護指定的類的成員的名稱(如果他們不會在壓縮步驟中刪除)
-keepclasseswithmembers {class_specification}    保護指定的類和類的成員,但條件是所有指定的類和類成員是要存在
-keepclasseswithmembernames {class_specification}    保護指定的類和類的成員的名稱,如果所有指定的類成員出席(在壓縮步驟之後)
-printseeds {filename}    列出類和類的成員-keep選項的清單,標准輸出到給定的文件
-keepattributes {attribute_name,...}    保護給定的可選屬性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, and InnerClasses.

壓縮相關
-dontshrink    不壓縮輸入的類文件
-printusage {filename}
-dontwarn   如果有警告也不終止
-whyareyoukeeping {class_specification}

優化相關
-dontoptimize    不優化輸入的類文件
-assumenosideeffects {class_specification}    優化時假設指定的方法,沒有任何副作用
-allowaccessmodification    優化時允許訪問並修改有修飾符的類和類的成員

混淆相關
-dontobfuscate    不混淆輸入的類文件
-printmapping {filename}
-applymapping {filename}    重用映射增加混淆
-obfuscationdictionary {filename}    使用給定文件中的關鍵字作為要混淆方法的名稱
-overloadaggressively    混淆時應用侵入式重載
-useuniqueclassmembernames    確定統一的混淆類的成員名稱來增加混淆
-flattenpackagehierarchy {package_name}    重新包裝所有重命名的包並放在給定的單一包中
-repackageclass {package_name}    重新包裝所有重命名的類文件中放在給定的單一包中
-dontusemixedcaseclassnames    混淆時不會產生形形色色的類名
-renamesourcefileattribute {string}    設置源文件中給定的字符串常量
關鍵字 描述 keep 保留類和類中的成員,防止它們被混淆或移除。 keepnames 保留類和類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除。 keepclassmembers 只保留類中的成員,防止它們被混淆或移除。 keepclassmembernames 只保留類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除。 keepclasseswithmembers 保留類和類中的成員,防止它們被混淆或移除,前提是指名的類中的成員必須存在,如果不存在則還是會混淆。 keepclasseswithmembernames 保留類和類中的成員,防止它們被混淆,但當成員沒有被引用時會被移除,前提是指名的類中的成員必須存在,如果不存在則還是會混淆。 通配符 描述 匹配類中的所有字段 匹配類中的所有方法 匹配類中的所有構造函數 * 匹配任意長度字符,但不含包名分隔符(.)。比如說我們的完整類名是com.vise.note.MainActivity,使用com.*,或者com.vise.*都是無法匹配的,因為*無法匹配包名中的分隔符,正確的匹配方式是com.vise.*.*,或者com.vise.note.*,這些都是可以的。但如果你不寫任何其它內容,只有一個*,那就表示匹配所有的東西。 ** 匹配任意長度字符,並且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有內容,包括任意長度的子包。 *** 匹配任意參數類型。比如void set*(***)就能匹配任意傳入的參數類型,*** get*()就能匹配任意返回值的類型。 匹配任意長度的任意類型參數。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)這些方法。
3、混淆文件編寫

混淆是apk上線前挺重要的一個環節,Android使用的是ProGuard,可以起到壓縮,混淆,預檢,優化的作用。縱觀大部分項目的混淆文件,其大部分內容都是固定的,從中可以整理出一個通用的模板,模板內容大致分為以下幾個部分:基本指令、公共組件、第三方庫、實體類、反射相關及JS調用相關。其中前兩部分基本不會有太大變化,第三方庫網上基本都會提供混淆方式,下文也會依據網上資源整理出大部分的三方庫保留方式,而後面幾個部分就與具體項目相關了,掌握思路後依照具體項目定制就行。

3.1、基本指令

-optimizationpasses 5   # 指定代碼的壓縮級別
-dontusemixedcaseclassnames # 表示混淆時不使用大小寫混合類名
-dontskipnonpubliclibraryclasses    # 表示不跳過library中的非public的類
-dontskipnonpubliclibraryclassmembers   # 指定不去忽略包可見的庫類的成員
-dontoptimize   # 表示不進行優化,建議使用此選項,因為根據proguard-android-optimize.txt中的描述,優化可能會造成一些潛在風險,不能保證在所有版本的Dalvik上都正常運行
-dontpreverify  # 表示不進行預校驗,這個預校驗是作用在Java平台上的,Android平台上不需要這項功能,去掉之後還可以加快混淆速度
-verbose    # 表示打印混淆的詳細信息
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*    # 混淆時所采用的算法

-dump dump.txt  # 描述apk內所有class文件的內部結構
-printseeds seeds.txt   # 列出了沒有被混淆的類和成員
-printusage unused.txt  # 列出了源代碼中被刪除在apk中不存在的代碼
-printmapping mapping.txt   # 表示混淆前後代碼的對照表

3.2、公共組件

-keep public class * extends android.app.Activity   # 保留繼承自Activity類不被混淆
-keep public class * extends android.app.Application    # 保留繼承自Application類不被混淆
-keep public class * extends android.support.multidex.MultiDexApplication   # 保留繼承自MultiDexApplication類不被混淆
-keep public class * extends android.app.Service    # 保留繼承自Service類不被混淆
-keep public class * extends android.content.BroadcastReceiver  # 保留繼承自BroadcastReceiver類不被混淆
-keep public class * extends android.content.ContentProvider    # 保留繼承自ContentProvider類不被混淆
-keep public class * extends android.app.backup.BackupAgentHelper   # 保留繼承自BackupAgentHelper類不被混淆
-keep public class * extends android.preference.Preference  # 保留繼承自Preference類不被混淆
-keep public class com.google.vending.licensing.ILicensingService   # 保留Google包下ILicensingService類不被混淆
-keep public class com.android.vending.licensing.ILicensingService  # 保留Android包下ILicensingService類不被混淆

-keepattributes *Annotation*,InnerClasses,Signature,SourceFile,LineNumberTable  # 保留相關屬性

-keepclasseswithmembernames class * {   # 保持native方法不被混淆
    native ;
}

-keepclassmembers public class * extends android.view.View{ # 保持自定義控件類不被混淆
    *** get*();
    void set*(***);
    public (android.content.Context);
    public (android.content.Context, android.util.AttributeSet);
    public (android.content.Context, android.util.AttributeSet, int);
}

-keepclasseswithmembers class * {   # 保持自定義控件類不被混淆
    public (android.content.Context, android.util.AttributeSet);
    public (android.content.Context, android.util.AttributeSet, int);
}

-keepclassmembers class * extends android.app.Activity {    # 表示不混淆Activity中參數是View的方法,主要針對在xml中配置onClick事件
   public void *(android.view.View);
}

-keepclassmembers enum * {  # 保持枚舉類不被混淆
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {    # 保持Parcelable不被混淆
  public static final android.os.Parcelable$Creator *;
}

-keepclassmembers class * implements java.io.Serializable { # 保持Serializable不被混淆
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

-keepclassmembers class **.R$* {   # 表示不混淆R文件下的靜態字段
    public static ;
}

-keepclassmembers class * extends android.webkit.webViewClient {    # 保留WebView
    public void *(android.webkit.webView, jav.lang.String);
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}

3.3、第三方庫

#EventBus
-keepattributes *Annotation*
-keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    (java.lang.Throwable);
}
-keepclassmembers class * { # 保持EventBus中Event事件接收
    void *(**On*Event);
}
#注:此處以EventBus為例,描述第三方庫的保留方式,更多的第三方庫保留方式已提交到github上。

3.4、實體類

-keep class 你的實體類包名.** { *; }

3.5、反射相關

-keep class 你的類所在的包.** { *; }

3.6、JS調用相關

-keepattributes *JavascriptInterface*
-keep class **.Webview2JsInterface { *; }  # 保持WebView對HTML頁面的API不被混淆
-keepclassmembers class fqcn.of.javascript.interface.for.webview {  # 保留WebView
   public *;
}
-keep class 你的類所在的包.** { *; }
#如果是內部類則使用如下方式
-keepclasseswithmembers class 你的類所在的包.父類$子類 { ; }

到此混淆相關介紹就已完成,完整的混淆文件已提交到github上,路徑如下:https://github.com/xiaoyaoyou1212/Note/blob/master/app/proguard-rules.pro.其中包含了大部分第三方庫的混淆方式。

注:混淆後生成的相關信息文件在當前module的build/outputs/mapping下,目錄下可能會有渠道名,下面再分為debug和release,與你的配置相關,其中的mapping.txt文件需要重點關注,該文件表示混淆前後代碼的對照表,這個文件非常重要,如果你的代碼混淆後會產生bug的話,log提示中是混淆後的代碼,希望定位到源代碼的話就可以根據mapping.txt反推,每次發布都要保留它方便該版本出現問題時調出日志進行排查,它可以根據版本號或是發布時間命名來保存或是放進代碼版本控制中。

二、APK瘦身

1、APK文件結構

Android應用是用Java編寫的,利用Android SDK編譯代碼,並且把所有的數據和資源文件打包成一個APK (Android Package)文件,這是一個後綴名為.apk的壓縮文件,APK文件中包含了一個Android應用程序的所有內容,是Android平台用於安裝應用程序的文件。APK就是一個zip壓縮包,解開這個APK包我們可以看到以下的結構:

目錄 描述 assets目錄 存放需要打包到apk中的靜態文件 lib目錄 程序依賴的native庫 res目錄 存放應用程序的資源 META-INF目錄 存放應用程序簽名和證書的目錄 AndroidManifest.xml 應用程序的配置文件 classes.dex dex可執行文件 resources.arsc 資源配置文件 assets目錄
用於存放需要打包到APK中的靜態文件,和res的不同點在於,assets目錄支持任意深度的子目錄,用戶可以根據自己的需求任意部署文件夾架構,而且res目錄下的文件會在R文件中生成對應的資源ID,assets不會自動生成對應的ID,訪問的時候需要AssetManager類。 lib目錄
這裡存放應用程序依賴的native庫文件,一般是用C/C++編寫,這裡的lib庫可能包含4中不同類型,根據CPU型號的不同,大體可以分為ARM、ARM-v7a、MIPS、X86,分別對應著ARM架構,ARM-V7架構,MIPS架構和X86架構,而每個架構又分32位和64位;其中,不同的CPU架構對應著不同的目錄,每個目錄中可以放很多對應版本的so庫,且這個目錄的結構固定,用戶只能按照這個目錄存放自己的so庫。目前市場上使用的移動終端大多是基於ARM或者ARM-V7a架構的,X86和MIPS架構的移動智能終端比較少,所以有些應用程序lib目錄下只包含armeabi目錄或者armeabi-v7a目錄,也就是說,這四個目錄要根據CPU的架構來選,而市面上ARM架構的手機占大多數,所以一般的APK只包含ARM和ARM-V7a的so。 res目錄
res是resource的縮寫,這個目錄存放資源文件,存在這個文件夾下的所有文件都會映射到Android工程的R文件中,生成對應的ID,訪問的時候直接使用資源ID即R.id.filename,res文件夾下可以包含多個文件夾,其中anim存放動畫文件;drawable目錄存放圖像資源;layout目錄存放布局文件;values目錄存放一些特征值,colors.xml存放color顏色值,dimens.xml定義尺寸值,string.xml定義字符串的值,styles.xml定義樣式對象;xml文件夾存放任意xml文件,在運行時可以通過Resources.getXML()讀取;raw是可以直接復制到設備中的任意文件,無需編譯。 META-INF目錄
保存應用的簽名信息,簽名信息可以驗證APK文件的完整性。AndroidSDK在打包APK時會計算APK包中所有文件的完整性,並且把這些完整性保存到META-INF文件夾下,應用程序在安裝的時候首先會根據META-INF文件夾校驗APK的完整性,這樣就可以保證APK中的每一個文件都不能被篡改。以此來確保APK應用程序不被惡意修改或者病毒感染,有利於確保Android應用的完整性和系統的安全性。META-INF目錄下包含的文件有CERT.RSA,CERT.DSA,CERT.SF和MANIFEST.MF,其中CERT.RSA是開發者利用私鑰對APK進行簽名的簽名文件,CERT.SF,MANIFEST.MF記錄了文件中文件的SHA-1哈希值。 AndroidManifest.xml
是Android應用程序的配置文件,是一個用來描述Android應用“整體資訊”的設定文件,簡單來說,相當於Android應用向Android系統“自我介紹”的配置文件,Android系統可以根據這個“自我介紹”完整地了解APK應用程序的資訊,每個Android應用程序都必須包含一個AndroidManifest.xml文件,且它的名字是固定的,不能修改。我們在開發Android應用程序的時候,一般都把代碼中的每一個Activity,Service,ContentProvider和BroadcastReceiver在AndroidManifest.xml中注冊,只有這樣系統才能啟動對應的組件,另外這個文件還包含一些權限聲明以及使用的SDK版本信息等等。程序打包時,會把AndroidManifest.xml進行簡單的編譯,便於Android系統識別,編譯之後的格式是AXML格式。 classes.dex
傳統的Java程序,首先先把Java文件編譯成class文件,字節碼都保存在了class文件中,Java虛擬機可以通過解釋執行這些class文件。而Dalvik虛擬機是在Java虛擬機進行了優化,執行的是Dalvik字節碼,而這些Dalvik字節碼是由Java字節碼轉換而來,一般情況下,Android應用在打包時通過AndroidSDK中的dx工具將Java字節碼轉換為Dalvik字節碼。dx工具可以對多個class文件進行合並,重組,優化,可以達到減小體積,縮短運行時間的目的。 resources.arsc
用來記錄資源文件和資源ID之間的映射關系,用來根據資源ID尋找資源。Android的開發是分模塊的,res目錄專門用來存放資源文件,當在代碼中需要調用資源文件時,只需要調用findviewbyId()就可以得到資源文件,每當在res文件夾下放一個文件,aapt就會自動生成對應的ID保存在R文件中,我們調用這個ID就可以,但是只有這個ID還不夠,R文件只是保證編譯程序不報錯,實際上在程序運行時,系統要根據ID去尋找對應的資源路徑,而resources.arsc文件就是用來記錄這些ID和資源文件位置對應關系的文件。

從以上目錄結構中可以看出,如果需要縮小apk的大小,主要針對的是assets目錄、lib目錄、res目錄及classes.dex文件。其中的assets目錄主要是靜態加載到apk中的文件,這個可以根據是否需要來進行手動管理,沒什麼優化的空間,下文主要針對classes.dex文件、res目錄及lib目錄來進行優化講解。

2、Apk瘦身方式

2.1、Proguard混淆優化代碼
Proguard是一個很強悍的工具,它可以幫你在代碼編譯時對代碼進行混淆,優化和壓縮。它有一個專門用來減少apk文件大小的功能叫做tree-shaking。Proguard 會遍歷你的所有代碼然後找出無用處的代碼。所有這些不可達(或者不需要)的代碼都會在生成最終的apk文件之前被清除掉。Proguard 也會重命名你的類屬性,類和接口,然整個代碼盡可能地保持輕量級水平。

2.2、Lint檢查優化資源
混淆只能優化java代碼,不能對無用資源進行清理,而Lint則可以檢查所有無用的資源文件,只要使用命令./gradlew lint或者在Android Studio工程中點擊Analyze->Inspect Code,選擇Whole Project點擊ok就行。它在檢測完之後會提供一份詳細的資源文件清單,並將無用的資源列在“UnusedResources: Unused resources” 區域之下。只要你不通過反射來反問這些無用資源,你就可以放心地移除這些文件了。

2.3、壓縮圖片
圖片資源的優化原則是:在不降低圖片效果、保證APK顯示效果的前提下縮小圖片文件的大小。

AAPT:Aapt(Android Asset Packaging Tool)就內置了保真圖像壓縮算法。例如,一個只需256色的真彩PNG圖片會被aapt通過一個顏色調色板轉化成一個8-bit PNG文件,這可以幫助你減少圖片文件的大小。 使用tinypng優化大部分圖片資源:tinypng是一個支持壓縮png和jpg圖片格式的網站,通過其獨特的算法(通過一種叫“量化”的技術,把原本png文件的24位真彩色壓縮為8位的索引演示,是一種矢量壓縮方法,把顏色值用數值123等代替。)可以實現在無損壓縮的情況下圖片文件大小縮小到原來的30%-50%。
tinypng的缺點是在壓縮某些帶有過渡效果(帶alpha值)的圖片時,圖片會失真,這種圖片可以將png圖片轉換為下面介紹的webP格式,可以在保證圖片質量的前提下大幅縮小圖片的大小。 使用webP圖片格式:WebP是谷歌研發出來的一種圖片數據格式,它是一種支持有損壓縮和無損壓縮的圖片文件格式,派生自圖像編碼格式 VP8。根據 Google 的測試,無損壓縮後的 WebP 比 PNG 文件少了 45% 的文件大小,即使這些 PNG 文件經過其他壓縮工具壓縮之後,WebP 還是可以減少 28% 的文件大小。目前很多公司已經將webP技術運用到Android APP中,比如FaceBook、騰訊、淘寶。webP相比於png最明顯的問題是加載稍慢,不過現在的智能設備硬件配置越來越高,這都不是事兒。
假如你打算在 App 中使用 WebP,除了 Android4.0 以上提供的原生支持外,其他版本以可以使用官方提供的解析庫webp-android-backport編譯成so使用。
通常UI提供的圖片都是png或者jpg格式,我們可以通過智圖或者isparta將其它格式的圖片轉換成webP格式,isparta可實現批量轉換,牆裂推薦! 使用tintcolor實現按鈕反選效果:通常按鈕的正反旋圖片我們都是通過提供一張按鈕正常圖片和一張按鈕反選圖片,然後通過selector實現,兩張圖片除了alpha值不一樣外其它的內容都是重復的,在Android 5.0及以上的版本可以通過tintcolor實現只提供一張按鈕的圖片,在程序中實現按鈕反選效果,前提是圖片的內容一樣,只是正反選按鈕的顏色不一樣。

2.4、限制支持CPU架構
一般說來Android使用Java代碼即可以滿足大部分需求,不過還是有一小部分案例需要使用一些native code。CPU的架構主要分為以下幾種:ARM架構,ARM-V7架構,MIPS架構和X86架構,目前市場上使用的移動終端大多是基於ARM或者ARM-V7a架構的,X86和MIPS架構的移動智能終端比較少,所以有些應用程序lib目錄下只包含armeabi目錄或者armeabi-v7a目錄,也就是說lib目錄要根據CPU的架構來選,而市面上ARM架構的手機占大多數,所以一般的APK只包含ARM和ARM-V7a的so。

2.5、其他優化技巧

對資源文件進行取捨
Android支持多種設備。Android的系統設計讓它可以支持設備的多樣性:屏幕密度,屏幕形狀,屏幕大小等等。到了Android4.4,它支持的屏幕密度包括:ldpi,mdpi,hdpi,xhdpi,xxhdpi以及xxxhdpi,但Android支持這麼多的屏幕密度並不意味著需要為每一個屏幕密度提供相應的資源文件。我們可以選擇目前主流手機的分辨率對應的xhdpi和xxhdpi,如果某些設備不是這幾個屏幕密度的,不用擔心,Android系統會自動使用存在的資源為設備計算然後提供相近大小的資源文件。 資源文件最少化配置
Android開發經常會依賴各種外部開源代碼庫,但是這些庫裡面並不是所有的資源文件你都會用到。從Android Gradle Plugin 0.7 開始,你可以配置你的app的build系統。這主要是通過配置resConfig和resConfigs以及默認的配置選項。下面的DSL(Domain Specific Language)就會阻止aapt(Android Asset Packaging Tool)打包app中不需要的資源文件。
defaultConfig {
    resConfigs "en", "de", "fr", "it"
    resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
盡可能地重用
重用資源是最重要的優化技巧之一。比如在一個ListView或者RecyclerView,重用可以幫助你在列表滾動時保持界面流暢;重用還可以幫你減少apk文件的大小,例如,Android 提供了幾個工具為一個asset文件重新著色,在Android L中你可以使用android:tint和android:tintMode來達到效果,在老版本中則可以使用ColorFilter;如果系統中有兩種圖片,一種圖片是另一種圖片翻轉180°得到的,那麼你就可以移除一種圖片,通過代碼實現。 在合適的時候使用代碼渲染圖像
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved