Android效能優化 上
說明
這篇文章是將很久以來看過的文章,包括自己寫的一些測試程式碼的總結.屬於筆記的性質,沒有面面俱到,一些自己相對熟悉的點可能會略過.
最開始看到的效能優化的文章,就是胡凱的優化典範系列,後來又陸續看過一些人寫的,個人覺得anly_jun和胡凱的質量最好.
文章大的框架也是先把優化典範過一遍,記錄個人認為重要的點,然後是anly_jun的系列,將之前未覆蓋的補充進去,也包括HenCoder的一些課程相關內容.
當然除了上面幾位,還有很多其他大神的文章,時間久了也記不太清,在此一併謝過.
筆記內容引用來源
1.Android效能優化之渲染篇
1.VSYNC
- 幀率:GPU在1秒內繪製操作的幀數.如60fps.
- 我們通常都會提到60fps與16ms,這是因為人眼與大腦之間的協作無法感知超過60fps的畫面更新.
- 開發app的效能目標就是保持60fps,這意味著每一幀只有16ms=1000/60的時間來處理所有的任務
- 重新整理率:螢幕在1秒內重新整理螢幕的次數.如60Hz,每16ms重新整理1次螢幕.
- GPU獲取圖形資料進行渲染,然後螢幕將渲染後的內容展示在螢幕上.
- 大多數手機螢幕的重新整理率是60Hz,如果GPU渲染1幀的時間低於1000/60=16ms,那麼在螢幕重新整理時候都有最新幀可顯示.如果GPU渲染某1幀 f 的時間超過16ms,在螢幕重新整理時候,f並沒有被GPU渲染完成則無法展示,螢幕只能繼續展示f的上1幀的內容.這就是掉幀,造成了UI介面的卡頓.
下面展示了幀率正常和幀率低於重新整理率(掉幀)的情形

image

image
2.GPU渲染:GPU渲染依賴2個元件:CPU和GPU

image

image
- CPU負責Measure,Layout,Record,Execute操作.
- GPU負責Rasterization(柵格化)操作.
- 為了App流暢,我們需要確保在16ms內完成所有CPU和GPU的工作.
3.過度繪製
Overdraw過度繪製是指螢幕上的某個畫素在同一幀的時間內被繪製了多次.過度繪製會大量浪費CPU及GPU資源/佔用CPU和GPU的處理時間
- 過度繪製的原因
- UI佈局存在大量重疊
- 非必須的背景重疊.
- 如Activity有背景,Layout又有背景,子View又有背景.僅僅移除非必要背景就可以顯著提升效能.
- 子View在onDraw中存在重疊部分繪製的情況,比如Bitmap重疊繪製
4.如何提升渲染效能
- 移除XML佈局檔案中非必要的Background
- 保持佈局扁平化,儘量避免佈局巢狀
- 在任何時候都避免呼叫requestLayout(),呼叫requestLayout會導致該layout的所有父節點都發生重新layout的操作
- 在自定義View的onDraw中避免過度繪製.
程式碼例項:
public class OverdrawView extends View { public OverdrawView(Context context) { super(context); init(); } public OverdrawView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public OverdrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Bitmap bitmap1,bitmap2,bitmap3; private void init(){ paint.setStyle(Paint.Style.FILL); bitmap1 = BitmapFactory.decodeResource(getResources(),R.mipmap.png1); bitmap2 = BitmapFactory.decodeResource(getResources(),R.mipmap.png2); bitmap3 = BitmapFactory.decodeResource(getResources(),R.mipmap.png3); } int w,h; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); w = getMeasuredWidth(); h = getMeasuredHeight(); } private boolean Overdraw = true; @Override protected void onDraw(Canvas canvas) { if(Overdraw){ //預設會出現過度繪製 canvas.drawBitmap(bitmap1,0,0,paint); canvas.drawBitmap(bitmap2,w/3,0,paint); canvas.drawBitmap(bitmap3,w*2/3,0,paint); }else{ //使用Canvas.clipRect避免過度繪製 canvas.save(); canvas.clipRect(0,0,w/3,h); canvas.drawBitmap(bitmap1,0,0,paint); canvas.restore(); canvas.save(); canvas.clipRect(w/3,0,w*2/3,h); canvas.drawBitmap(bitmap2,w/3,0,paint); canvas.restore(); canvas.save(); canvas.clipRect(w*2/3,0,w,h); canvas.drawBitmap(bitmap3,w*2/3,0,paint); canvas.restore(); } } //切換是否避免過度繪製 public void toggleOverdraw(){ Overdraw = !Overdraw; invalidate(); } }
效果圖:

過度繪製

避免過度繪製
2.Android效能優化之記憶體篇
1.Android虛擬機器的 分代堆記憶體/Generational Heap Memory模型

image

image
- 和JVM不同:Android的堆記憶體多了1個永久代/Permanent Generation.
- 和JVM類似:
- 新建立的物件儲存在新生代/Young Generation
- GC所佔用的時間和它是哪一個Generation有關,Young Generation的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長
- 無論哪一代,觸發GC後,所有非垃圾回收執行緒暫停,GC結束後所有執行緒恢復執行
- 如果短時間內進行過多GC,多次暫停執行緒進行垃圾回收的累積時間就會增大.佔用過多的幀間隔時間/16ms,導致CPU和GPU用於計算渲染的時間不足,導致卡頓/掉幀.
2.記憶體洩漏和記憶體溢位
記憶體洩漏就是無用物件佔據的記憶體空間沒有及時釋放,導致記憶體空間浪費的情況.memory leak.
記憶體溢位是App為1個物件申請記憶體空間,記憶體空間不足的情況.out of memory.
記憶體洩漏數量足夠大,就會引起記憶體溢位.或者說記憶體洩漏是記憶體溢位的原因之一.
3.Android效能優化典範-第2季
1.提升動畫效能
-
Bitmap的縮放,旋轉,裁剪比較耗效能.例如在一個圓形的鐘表圖上,我們把時鐘的指標摳出來當做單獨的圖片進行旋轉會比旋轉一張完整的圓形圖效能好.
image
- 儘量減少每次重繪的元素可以極大提升效能.可以把複雜的View拆分會更小的View進行組合,在需要重新整理介面時候僅對指定View進行重繪.
- 假如鐘錶介面上有很多元件,可以把這些元件做拆分,背景圖片單獨拎出來設定為一個獨立的View,通過setLayerType()方法使得這個View強制用Hardware來進行渲染.至於介面上哪些元素需要做拆分,他們各自的更新頻率是多少,需要有針對性的單獨討論
2.物件池
- 短時間內大量物件被建立然後很快被銷燬,會多次觸發Android虛擬機器在Young generation進行GC,使用AS檢視記憶體曲線,會看到記憶體曲線劇烈起伏,稱為"記憶體抖動".
- GC會暫停其他執行緒,短時間多次GC/記憶體抖動會引起CPU和GPU在16ms內無法完成當前幀的渲染,引起介面卡頓.
- 避免記憶體抖動,可以使用物件池
- 例項
public class User { public String id; public String name; //物件池例項 private static final SynchronizedPool<User> sPool = new SynchronizedPool<User>(10); public static User obtain() { User instance = sPool.acquire(); return (instance != null) ? instance : new User(); } public void recycle() { sPool.release(this); } }
3.for index,for simple,iterator三種遍歷效能比較
public class ForTest { public static void main(String[] args) { Vector<Integer> v = new Vector<>(); ArrayList<Integer> a = new ArrayList<>(); LinkedList<Integer> l = new LinkedList<>(); int time = 1000000; for(int i = 0; i< time; i++){ Integer item = new Random().nextInt(time); v.add(item); a.add(item); l.add(item); } //測試3種遍歷效能 long start = System.currentTimeMillis(); for(int i = 0;i<v.size();i++){ Integer item = v.get(i); } long end = System.currentTimeMillis(); System.out.println("for index Vector耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(int i = 0;i<a.size();i++){ Integer item = a.get(i); } end = System.currentTimeMillis(); System.out.println("for index ArrayList耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(int i = 0;i<l.size();i++){ Integer item = l.get(i); } end = System.currentTimeMillis(); System.out.println("for index LinkedList耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Integer item:v){ Integer i = item; } end = System.currentTimeMillis(); System.out.println("for simple Vector耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Integer item:a){ Integer i = item; } end = System.currentTimeMillis(); System.out.println("for simple ArrayList耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Integer item:l){ Integer i = item; } end = System.currentTimeMillis(); System.out.println("for simple LinkedList耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Iterator i = v.iterator();i.hasNext();){ Integer item = (Integer) i.next(); } end = System.currentTimeMillis(); System.out.println("for Iterator Vector耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Iterator i = a.iterator();i.hasNext();){ Integer item = (Integer) i.next(); } end = System.currentTimeMillis(); System.out.println("for Iterator ArrayList耗時:"+(end-start)+"ms"); start = System.currentTimeMillis(); for(Iterator i = l.iterator();i.hasNext();){ Integer item = (Integer) i.next(); } end = System.currentTimeMillis(); System.out.println("for Iterator LinkedList耗時:"+(end-start)+"ms"); } } 列印結果: for index Vector耗時:28ms for index ArrayList耗時:14ms LinkedList就不能用for index方式進行遍歷. for simple Vector耗時:68ms for simple ArrayList耗時:11ms for simple LinkedList耗時:34ms for Iterator Vector耗時:49ms for Iterator ArrayList耗時:12ms for Iterator LinkedList耗時:0ms
- 不要用for index去遍歷連結串列,因為LinkedList在get任何一個位置的資料的時候,都會把前面的資料走一遍.應該使用Iterator去遍歷
- get(0),直接拿到0位的Node0的地址,拿到Node0裡面的資料
- get(1),直接拿到0位的Node0的地址,從0位的Node0中找到下一個1位的Node1的地址,找到Node1,拿到Node1裡面的資料
- get(2),直接拿到0位的Node0的地址,從0位的Node0中找到下一個1位的Node1的地址,找到Node1,從1位的Node1中找到下一個2位的Node2的地址,找到Node2,拿到Node2裡面的資料
- Vector和ArrayList,使用for index遍歷效率較高
4.Merge:通過Merge減少1個View層級
-
可以將merge當做1個ViewGroup v,如果v的型別和v的父控制元件的型別一致,那麼v其實沒必要存在,因為白白增加了佈局的深度.所以merge使用時必須保證merge中子控制元件所應該在的ViewGroup型別和merge所在的父控制元件型別一致.
-
Merge的使用場景有2個:
- Activity的佈局檔案的根佈局是FrameLayout,則將FrameLayout替換為merge
- 因為setContentView本質就是將佈局檔案inflate後加載到了id為android.id.content的FrameLayout上.
- merge作為根佈局的佈局檔案通過include標籤被引入其他佈局檔案中.這時候include所在的父控制元件,必須和merge所在的佈局檔案"原本根佈局"一致.
- Activity的佈局檔案的根佈局是FrameLayout,則將FrameLayout替換為merge
-
程式碼示例
merge作為根佈局的佈局檔案,用於Activity的setContentView:
activity_merge.xml <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="0dp" android:text="111111" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:layout_marginLeft="40dp" android:text="222222" /> </merge>
merge作為根佈局的佈局檔案,被include標籤引入其他佈局檔案中:
activity_merge_include.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="merge被include引用" /> <include layout="@layout/activity_merge" /> </LinearLayout>
5.使用.9.png作為背景
- 典型場景是1個ImageView需要新增1個背景圖作為邊框.這樣邊框所在矩形的中間部分和實際顯示的圖片就好重疊發生Overdraw.
- 可以將背景圖製作成.9.png.和前景圖重疊部分設定為透明.Android的2D渲染器會優化.9.png的透明區域.
6.減少透明區域對效能的影響
- 不透明的View,顯示它只需要渲染一次;如果View設定了alpha值,會至少需要渲染兩次,效能不好
- 設定透明度setAlpha的時候,會把當前view繪製到offscreen buffer中,然後再顯示出來.offscreen buffer是 一個臨時緩衝區,把View放進來並做透明度的轉化,然後顯示到螢幕上,這個過程效能差,所以應該儘量避免這個過程
- 如何避免使用offscreen buffer
- 對於不存在過度繪製的View,如沒有背景的TextView,就可以直接設定文字顏色;ImageView設定圖片透明度setImageAlpha;自定義View設定繪製時的paint的透明度
- 如果是自定義View,確定不存在過度繪製,可以重寫hasOverlappingRendering返回false即可.這樣設定alpha時android會自動優化,避免使用offscreen buffer.
@Override public boolean hasOverlappingRendering() { return false; }
- 如果不是1,2兩種情況,要設定View的透明度,則需要讓GPU來渲染指定View,然後再設定透明度.
View v = findViewById(R.id.root); //通過setLayerType的方法來指定View應該如何進行渲染 //開啟硬體加速 v.setLayerType(View.LAYER_TYPE_HARDWARE,null); v.setAlpha(0.60F); //透明度設定完畢後關閉硬體加速 v.setLayerType(View.LAYER_TYPE_NONE,null);
4.Android效能優化典範-第3季
1.避免使用列舉,用註解進行替代
- 列舉的問題
- 每個列舉值都是1個物件,相比較Integer和String常量,列舉的記憶體開銷至少是其2倍.
- 過多列舉會增加dex大小及其中的方法數量,增加App佔用的空間及引發65536機率
- 如何替代列舉:使用註解
- android.support.annotation 中的@IntDef,@StringDef來包裝Integer和String常量.
- 3個步驟
- 首先定義常量
- 然後自定義註解,設定取值範圍就是剛剛定義的常量,並設定自定義註解的保留範圍為原始碼時/SOURCE
- 位指定的屬性及方法新增自定義註解.
- 程式碼例項
public class MainActivity extends Activity { //1:首先定義常量 public static final int MALE = 0; public static final int FEMALE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); Person person = new Person(); person.setSex(MALE); ((Button) findViewById(R.id.test)).setText(person.getSexDes()); } class Person { //3.為指定的屬性及方法新增自定義註解 @SEX private int sex; //3.為指定的屬性及方法新增自定義註解 public void setSex(@SEX int sex) { this.sex = sex; } //3.為指定的屬性及方法新增自定義註解 @SEX public int getSex() { return sex; } public String getSexDes() { if (sex == MALE) { return "男"; } else { return "女"; } } } //2:然後建立自定義註解,設定取值範圍就是剛剛定義的常量,並設定自定義註解的保留範圍是原始碼時 @IntDef({MALE, FEMALE}) @Retention(RetentionPolicy.SOURCE) public @interface SEX { } }
5.Android記憶體優化之OOM
如何避免OOM:
- 減小物件的記憶體佔用
- 記憶體物件複用防止重建
- 避免記憶體洩漏
- 記憶體使用策略優化
1.減小物件的記憶體佔用
- 避免使用列舉,用註解替代
- 減小建立的Bitmap的記憶體,使用合適的縮放比例及解碼格式
- inSampleSize:縮放比例
- decode format:解碼格式
- 現在很多圖片資源的URL都可以新增圖片尺寸作為引數.在通過網路獲取圖片時選擇合適的尺寸,減小網路流量消耗,並減小生成的Bitmap的大小.
2.記憶體物件的重複利用
- 物件池技術:減少頻繁建立和銷燬物件帶來的成本,實現物件的快取和複用
- 儘量使用Android系統內建資源,可降低APK大小,在一定程度降低記憶體開銷
- ConvertView的複用
- LRU的機制實現Bitmap的快取(圖片載入框架的必備機制)
- 在for迴圈中,用StringBuilder代替String實現字串拼接
3.避免記憶體洩漏
- 在App中使用leakcanary檢測記憶體洩漏: leakcanary
- Activity的記憶體洩漏
- Handler引起Activity記憶體洩漏
- 原因:Handler作為Activity的1個非靜態內部類例項,持有Activity例項的引用.若Activity退出後Handler依然有待接收的Message,這時候發生GC,Message-Handler-Activity的引用鏈導致Activity無法被回收.
- 2種解決方法
-
在onDestroy呼叫Handler.removeCallbacksAndMessages(null)移除該Handler關聯的所有Message及Runnable.再發生GC,Message已經不存在,就可以順利的回收Handler及Activity
@Override protected void onDestroy() { super.onDestroy(); m.removeCallbacksAndMessages(null); }
-
自定義靜態內部類繼承Handler,靜態內部類例項不持有外部Activity的引用.在自定義Handler中定義外部Activity的弱引用,只有弱引用關聯的外部Activity例項未被回收的情況下才繼續執行handleMessage.自定義Handler持有外部Activity的弱引用,發生GC時不耽誤Activity被回收.
static class M extends Handler{ WeakReference<Activity> mWeakReference; public M(Activity activity) { mWeakReference=new WeakReference<Activity>(activity); } @Override public void handleMessage(Message msg) { if(mWeakReference != null){ Activity activity=mWeakReference.get(); if(activity != null){ if(msg.what == 15){ Toast.makeText(activity,"M:15",Toast.LENGTH_SHORT).show(); } if(msg.what == 5){ Toast.makeText(activity,"M:5",Toast.LENGTH_SHORT).show(); } } } } } private M m; @Override protected void onResume() { super.onResume(); m = new M(this); m.sendMessageDelayed(m.obtainMessage(15),15000); m.sendMessageDelayed(m.obtainMessage(5),5000); }
-
在避免記憶體洩漏的前提下,如果要求Activity退出就不執行後續動作,用方法1.如果要求後續動作在GC發生前繼續執行,使用方法2
-
- Handler引起Activity記憶體洩漏
- Context:儘量使用Application Context而不是Activity Context,避免不經意的記憶體洩漏
- 資源物件要及時關閉
4.記憶體使用策略優化
- 圖片選擇合適的資料夾進行存放
- hdpi/xhdpi/xxhdpi等等不同dpi的資料夾下的圖片在不同的裝置上會經過scale的處理。例如我們只在hdpi的目錄下放置了一張100100的圖片,那麼根據換算關係,xxhdpi的手機去引用那張圖片就會被拉伸到200200。需要注意到在這種情況下,記憶體佔用是會顯著提高的。對於不希望被拉伸的圖片,需要放到assets或者nodpi的目錄下
- 謹慎使用依賴注入框架.依賴注入框架會掃描程式碼,需要大量的記憶體空間對映程式碼.
- 混淆可以減少不必要的程式碼,類,方法等.降低對映程式碼所需的記憶體空間
- onLowMemory()與onTrimMemory():沒想到應該怎麼用
- onLowMemory
- 當所有的background應用都被kill掉的時候,forground應用會收到onLowMemory()的回撥.在這種情況下,需要儘快釋放當前應用的非必須的記憶體資源,從而確保系統能夠繼續穩定執行
- onTrimMemory(int level)
- 當系統記憶體達到某些條件的時候,所有正在執行的應用都會收到這個回撥,同時在這個回撥裡面會傳遞以下的引數,代表不同的記憶體使用情況,收到onTrimMemory()回撥的時候,需要根據傳遞的引數型別進行判斷,合理的選擇釋放自身的一些記憶體佔用,一方面可以提高系統的整體執行流暢度,另外也可以避免自己被系統判斷為優先需要殺掉的應用
- onLowMemory
6.Android開發最佳實踐
1.注意對隱式Intent的執行時檢查保護
- 類似開啟相機等隱式Intent,不一定能夠在所有的Android裝置上都正常執行.
- 例如系統相機應用被關閉或者不存在相機應用,或者某些許可權被關閉都可能導致丟擲ActivityNotFoundException的異常.
- 預防這個問題的最佳解決方案是在發出這個隱式Intent之前呼叫resolveActivity做檢查
- 程式碼例項
public class IntentCheckActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_intent_check); } public void openSBTest(View view) { // 跳轉到"傻逼"軟體 Intent sbIntent = new Intent("android.media.action.IMAGE_GO_SB"); if (sbIntent.resolveActivity(getPackageManager()) != null) { startActivity(sbIntent); } else { //會彈出這個提示 Toast.makeText(this,"裝置木有傻逼!",Toast.LENGTH_SHORT).show(); } } public void openCameraTest(View view) { // 跳轉到系統照相機 Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (cameraIntent.resolveActivity(getPackageManager()) != null) { startActivity(cameraIntent); //正常裝置會進入相機並彈出提示 Toast.makeText(this,"裝置有相機!",Toast.LENGTH_LONG).show(); } else { Toast.makeText(this,"裝置木有相機!",Toast.LENGTH_SHORT).show(); } } }
2.Android 6.0的許可權
3.MD新控制元件的使用:Toolbar替代ActionBar,AppBarLayout,Navigation Drawer, DrawerLayout, NavigationView等
7.Android效能優化典範-第4季
1.網路資料的快取.okHttp,Picasso都支援網路快取
2.程式碼混淆
2.1.AS中生成keystore.jks應用於APK打包
-
1:生成keystore.jks
image
-
2:檢視.jks檔案的SHA1安全碼
在AS的Terminal中輸入:
keytool -list -v -keystore C:\Users\Administrator\Desktop\key.jks
keytool -list -v -keystore .jks檔案詳細路徑
回車後,輸入金鑰庫口令/就是.jks的密碼,輸入過程不可見,輸入完畢回車即可!

image
2.2.proguard-rules關鍵字及部分萬用字元含義

2018-10-22_165523.png
- keep 完整類名{*;}, 可以對指定類進行完全保留,不混淆類名,變數名,方法名.
-keep public class a.b.c.TestItem{ *; }
- 在App中,我們會定義很多實體bean.往往涉及到bean例項和json字串間互相轉換.部分json庫會通過反射呼叫bean的set和get方法.因而實體bean的set,get方法不能被混淆,或者說我們自己寫的方法,如果會被第三方庫或其他地方通過反射呼叫,則指定方法要keep避免混淆.
-keep public class a.**.Bean.**{ public void set*(***); public *** get*(); # 對應獲取boolean型別屬性的方法 public *** is*(); }
- 我們自己寫的使用了反射功能的類,必須keep
#保留單個包含反射程式碼的類 -keep public class a.b.c.ReflectUtils{ *; } #保留所有包含反射程式碼的類,比如所有涉及反射程式碼的類都在a.b.reflectpackage包及其子包下 -keep class a.b.reflectpackage.**{ *; }
- 如果我們要保留繼承了指定類的子類,或者實現了指定介面的類
-keep class * extends a.b.c.Parent{*;} -keep class * implements a.b.c.OneInterface{*;}
2.3.proguard-rules.pro通用模板
#####################基本指令############################################## # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: -keepclassmembers class fqcn.of.javascript.interface.for.webview { public *; } # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. -renamesourcefileattribute SourceFile #程式碼混淆壓縮比,在0~7之間,預設為5,一般不需要改 -optimizationpasses 5 #混淆時不使用大小寫混合,混淆後的類名為小寫 -dontusemixedcaseclassnames #指定不去忽略非公共的庫的類 -dontskipnonpubliclibraryclasses #指定不去忽略非公共的庫的類的成員 -dontskipnonpubliclibraryclassmembers #不做預校驗,preverify是proguard的4個步驟之一 #Android不需要preverify,去掉這一步可加快混淆速度 -dontpreverify #有了verbose這句話,混淆後就會生成對映檔案 #包含有類名->混淆後類名的對映關係 -verbose #然後使用printmapping指定對映檔案的名稱 -printmapping mapping.txt #指定混淆時採用的演算法,後面的引數是一個過濾器,這個過濾器是谷歌推薦的演算法,一般不改變 -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* #保護程式碼中的Annotation不被混淆,這在JSON實體對映時非常重要(保留註解引數) -keepattributes *Annotation* #避免混淆泛型,這在JSON實體對映時非常重要 -keepattributes Signature #丟擲異常時保留程式碼行號 -keepattributes SourceFile,LineNumberTable #忽略所有警告 -ignorewarnings ###################需要保留的東西######################################## #保留反射的方法和類不被混淆================================================ #手動啟用support keep註解 #http://tools.android.com/tech-docs/support-annotations -keep class android.support.annotation.Keep -keep @android.support.annotation.Keep class * {*;} -keepclasseswithmembers class * { @android.support.annotation.Keep <methods>; } -keepclasseswithmembers class * { @android.support.annotation.Keep <fields>; } -keepclasseswithmembers class * { @android.support.annotation.Keep <init>(...); } #========================================================================================== #保留所有的本地native方法不被混淆 -keepclasseswithmembernames class * { native <methods>; } #保留了繼承自Activity、Application這些類的子類 -keep public class * extends android.app.Activity -keep public class * extends android.app.Fragment -keep public class * extends android.support.v4.app.Fragment -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class * extends android.view.View -keep public class * extends com.android.vending.licensing.ILicensingService -keep class android.support.** {*;} #保留在Activity中的方法引數是view的方法,從而我們在layout裡面便攜onClick就不會受影響 -keepclassmembers class * extends android.app.Activity{ public void *(android.view.View); } #列舉類不能被混淆 -keepclassmembers enum *{ public static **[] values(); public static ** valueOf(java.lang.String); } #保留自定義控制元件不被混淆 -keep public class * extends android.view.View { public <init>(android.content.Context); public <init>(android.content.Context, android.util.AttributeSet); public <init>(android.content.Context, android.util.AttributeSet, int); void set*(***); *** get*(); } #保留Parcelable序列化的類不被混淆 -keep class * implements android.os.Paracelable{ public static final android.os.Paracelable$Creator *; } #保留Serializable序列化的類的如下成員不被混淆 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient <fields>; !private <fields>; !private <methods>; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } #對於R(資源)下的所有類及其方法,都不能被混淆 -keep class **.R$*{ *; } #R檔案中的所有記錄資源id的靜態欄位 -keepclassmembers class **.R$* { public static <fields>; } #對於帶有回撥函式onXXEvent的,不能被混淆 -keepclassmembers class * { void *(**On*Event); } #============================針對app的量身定製============================================= # webView處理,專案中沒有使用到webView忽略即可 -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); public boolean *(android.webkit.WebView, java.lang.String); } -keepclassmembers class * extends android.webkit.webViewClient { public void *(android.webkit.webView, java.lang.String); }
2.4.混淆jar包
郭霖大神部落格有介紹,自己沒試過2.5.幾條實用的Proguard rules
在上面提供的通用模板上繼續新增下面幾行:
-repackageclasses com -obfuscationdictionary dict.txt -classobfuscationdictionary dict.txt -packageobfuscationdictionary dict.txt -assumenosideeffects class android.util.Log { public static boolean isLoggable(java.lang.String, int); public static int v(...); public static int i(...); public static int w(...); public static int d(...); public static int e(...); }
- repackageclasses:除了keep的類,會把我們自己寫的所有類以及所使用到的各種第三方庫程式碼統統移動到我們指定的單個包下.
- 比如一些比較敏感的被keep的類在包a.b.min下,我們可以使用 -repackageclasses a.b.min,這樣就有成千上萬的被混淆的類和未被混淆的敏感的類在a.b.min下面,正常人根本就找不到關鍵類.尤其是keep的類也只是保留關鍵方法,名字也被混淆過.
- -obfuscationdictionary,-classobfuscationdictionary和-packageobfuscationdictionary分別指定變數/方法名,類名,包名混淆後的字串集.
- 預設我們的程式碼命名會被混淆成字母組合,使用這些配置可以用亂碼或中文內容進行命名.中文命名可以破壞部分反編譯軟體的正常工作,亂碼則極大加大了檢視程式碼的難度.
- dict.txt:需要放到和app模組的proguard-rules.pro同級目錄.dict.txt具體內容可以自己寫,參考開源專案: 一種生成閱讀極其困難的proguard字典的演算法
- -assumenosideeffects class android.util.Log是在編譯成 APK 之前把日誌程式碼全部刪掉.
- Androidstudio 混淆去掉日誌 assumenosideeffects 不起作用
- 因為預設情況下,使用的是proguard-android.txt的混淆規則.proguard-android.txt中包含-dontoptimize.-dontoptimize導致日誌語句不會被優化掉.所以我們提供的模板不包含-dontoptimize這一句.
- Androidstudio 混淆去掉日誌 assumenosideeffects 不起作用
2.6.字串硬編碼
- 對於反編譯者來說,最簡單的入手點就是字串搜尋.硬編碼留在程式碼裡的字串值都會在反編譯過程中被原樣恢復,不要使用硬編碼.
- 如果一定要使用硬編碼
- 新建1個儲存硬編碼的常量類,靜態存放字串常量,即使找到了常量類,反編譯者很難搜尋到哪裡用了這些字串.
- 常量類中的靜態常量字串,用名稱作為真正內容,而值用難以理解的編碼表示.
//1:新建常量類,用於存放字串常量 public class HardStrings { //2:名稱是真正內容,值是難以理解的編碼. //這樣即使是必須儲存的Log,被反編譯者看到的也只是難以理解的值,搞不清意義 public static final String MaxMemory = "001"; public static final String M = "002"; public static final String MemoryClass = "003"; public static final String LargeMemoryClass = "004"; public static final String 系統總記憶體 = "005"; public static final String 系統剩餘記憶體 = "006"; public static final String 系統是否處於低記憶體執行 = "007"; public static final String 系統剩餘記憶體低於 = "008"; public static final String M時為低記憶體執行 = "009"; }
2.7.res資源混淆及多渠道打包
簡單講,使用騰訊的2個gradle外掛來實現res資源混淆及多渠道打包.
res資源混淆: AndResGuard
多渠道打包: VasDolly
具體流程:
AndResGuard使用了chaychan的方法,單獨建立gradle檔案
- 專案根目錄下build.gradle中,新增外掛的依賴,具體如下
buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.1.4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files //新增AndResGuard classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.12' //新增VasDolly classpath 'com.leon.channel:plugin:2.0.1' } }
- 在app目錄下單獨建立gradle檔案and_res_guard.gradle.內容如下
apply plugin: 'AndResGuard' andResGuard { mappingFile = null use7zip = true useSign = true keepRoot = false compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", "resources.arsc" ] whiteList = [ //// your icon //"R.drawable.icon", //// for fabric //"R.string.com.crashlytics.*", //// for umeng update //"R.string.tb_*", //"R.layout.tb_*", //"R.drawable.tb_*", //"R.drawable.u1*", //"R.drawable.u2*", //"R.color.tb_*", //// umeng share for sina //"R.drawable.sina*", //// for google-services.json //"R.string.google_app_id", //"R.string.gcm_defaultSenderId", //"R.string.default_web_client_id", //"R.string.ga_trackingId", //"R.string.firebase_database_url", //"R.string.google_api_key", //"R.string.google_crash_reporting_api_key", // ////友盟 //"R.string.umeng*", //"R.string.UM*", //"R.layout.umeng*", //"R.drawable.umeng*", //"R.id.umeng*", //"R.anim.umeng*", //"R.color.umeng*", //"R.style.*UM*", //"R.style.umeng*", // ////融雲 //"R.drawable.u*", //"R.drawable.rc_*", //"R.string.rc_*", //"R.layout.rc_*", //"R.color.rc_*", //"R.id.rc_*", //"R.style.rc_*", //"R.dimen.rc_*", //"R.array.rc_*" ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.12' //path = "/usr/local/bin/7za" } }
- 模組app下的build.gradle檔案新增依賴,具體如下
apply plugin: 'com.android.application' //引入剛剛建立的and_res_guard.gradle apply from: 'and_res_guard.gradle' //依賴VasDolly apply plugin: 'channel' channel{ //指定渠道檔案 channelFile = file("channel.txt") //多渠道包的輸出目錄,預設為new File(project.buildDir,"channel") baseOutputDir = new File(project.buildDir,"channel") //多渠道包的命名規則,預設為:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType} apkNameFormat ='${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}' //快速模式:生成渠道包時不進行校驗(速度可以提升10倍以上,預設為false) isFastMode = true //buildTime的時間格式,預設格式:yyyyMMdd-HHmmss buildTimeDateFormat = 'yyyyMMdd-HH:mm:ss' //低記憶體模式(僅針對V2簽名,預設為false):只把簽名塊、中央目錄和EOCD讀取到記憶體,不把最大頭的內容塊讀取到記憶體,在手機上合成APK時,可以使用該模式 lowMemory = false } rebuildChannel { //指定渠道檔案 channelFile = file("channel.txt") //baseDebugApk = new File(project.projectDir, "app-release_7zip_aligned_signed.apk") baseReleaseApk = new File(project.projectDir, "app-release_7zip_aligned_signed.apk") //預設為new File(project.buildDir, "rebuildChannel/debug") //debugOutputDir = new File(project.buildDir, "rebuildChannel/debug") //預設為new File(project.buildDir, "rebuildChannel/release") releaseOutputDir = new File(project.buildDir, "rebuildChannel/release") //快速模式:生成渠道包時不進行校驗(速度可以提升10倍以上,預設為false) isFastMode = false //低記憶體模式(僅針對V2簽名,預設為false):只把簽名塊、中央目錄和EOCD讀取到記憶體,不把最大頭的內容塊讀取到記憶體,在手機上合成APK時,可以使用該模式 lowMemory = false } android { signingConfigs { tcl { keyAlias 'qinghailongxin' keyPassword 'huanhailiuxin' storeFile file('C:/Users/Administrator/Desktop/key.jks') storePassword 'huanhailiuxin' } } compileSdkVersion 28 defaultConfig { applicationId "com.example.administrator.proguardapp" minSdkVersion 15 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled true shrinkResources true zipAlignEnabled true pseudoLocalesEnabled true proguardFiles 'proguard-rules.pro' signingConfig signingConfigs.tcl } debug { signingConfig signingConfigs.tcl minifyEnabled true pseudoLocalesEnabled true zipAlignEnabled true } } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:design:28.0.0' implementation 'com.android.support:support-vector-drawable:28.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation files('libs/litepal-2.0.0.jar') //依賴VasDolly api 'com.leon.channel:helper:2.0.1' }
- 首先使用AndResGuard實現資源混淆,再使用VasDolly實現多渠道打包
-
在Gradle介面中,找到app模組下andresguard的task.
- 如果想打debug包,則執行resguardDebug指令;
- 如果想打release包,則執行resguardRelease指令.
- 此處我們雙擊執行resguardRelease指令,在app目錄下的/build/output/apk/release/AndResGuard_{apk_name}/ 資料夾中找到混淆後的Apk,其中app-release_aligned_signed.apk為進行混淆並簽名過的apk.
-
我們檢視app-release_aligned_signed.apk,res資料夾更名為r,裡面的目錄名稱以及xml檔案已經被混淆.
image
-
將app-release_aligned_signed.apk放到app模組下,在Gradle介面中,找到app模組下channel的task,執行reBuildChannel指令.
-
雙擊執行reBuildChannel指令,幾秒鐘就生成了20個通過app-release_aligned_signed.apk的多渠道apk.
image
- 通過helper類庫中的ChannelReaderUtil類讀取渠道資訊
String channel = ChannelReaderUtil.getChannel(getApplicationContext());
-
-