1. 程式人生 > >Android 以apk包方式共享資源(動態換膚)的實現方式

Android 以apk包方式共享資源(動態換膚)的實現方式

一 應用場景:
之前的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作量,我把這些應用的資源統一起來 一起維護,相同的資源不需要維護2份,並且適配新資源(layout、drawable、多國語言等)工作量也能做到最小。另外, 當用戶想要更換面板介面時, 也只需要替換這一個資源包就可以了, 耦合度比較低。
二 優點:
1. 動態換膚; 2. 風格相同的一組應用可以共享一個資源包, 方便維護,也方便動態換膚, 想想都覺得爽! 三 缺點: 1. 有擴充套件控制元件需要配置到資源包中時, 宿主apk程式碼中無法從layout中獲取擴充套件控制元件並強制型別轉換,也就是說在宿主apk中無法使用擴充套件控制元件的擴充套件方法。原因在第六點:遇到的問題 中解釋。 
四 實現方式:
實現方式是使用 Context.createPackageContext(String packageName,  int flags)  方法來載入一個apk包的Context,有了Context後就可以得到Resources和LayoutInflater, 再通過反射機制可以得到資源ID。

把載入資源apk包的工作封裝到ResLoader中, 程式碼如下:

package com.example.resapktest;

import java.lang.reflect.Field;
import java.util.HashMap;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;

public class ResLoader
{
	private static final String TAG = "ResLoader";	
	private Context mContext = null;
	private Context mResApkContext = null;
	private LayoutInflater mResApkInflater = null;
	private String mResApkPackage = null;
	private ResIDMap mResIDMap = null;
	private Resources mResApkResources = null;

	public ResLoader(Context context)
	{
		this.mContext = context;
	}
	
	public View loadLayout(String resource)
	{
    	if (null != resource)		
    	{
    		int id = mResIDMap.mSkinIDMap.get(resource);
    		return mResApkInflater.inflate(id, null);			
		}
    	return null;
	}
	
	public View loadLayout(int id)
	{
		if(id <= 0) return null;
    	return mResApkInflater.inflate(id, null);
	}
	
	public int findIDByName(String resource)
	{
    	if (null == resource)
		{
			return -1;
		}
    	return mResIDMap.mSkinIDMap.get(resource);
	}
	
	public String getString(String resource)
	{
    	if (null == resource)
		{
			return null;
		}
		int id = mResIDMap.mSkinIDMap.get(resource);
		return mResApkContext.getString(id);
	}

	public float getDimension(String resource)
	{
    	if (null == resource)
		{
			return 0;
		}
		int id = mResIDMap.mSkinIDMap.get(resource);
		return mResApkResources.getDimension(id);
	}
	
	public Drawable getDrawable(String resource)
	{
    	if (null == resource)
		{
			return null;
		}
		int id = mResIDMap.mSkinIDMap.get(resource);
		return mResApkResources.getDrawable(id);
	}
	
	public Context getSkinContext()
	{
		return mResApkContext;
	}
	
	public ResIDMap getResIDMap()
	{
		return mResIDMap;
	}
	
	/**
	 * 
	* Title: loadResApk 
	* @Description:
	* @param resApk the res package name,such as "com.example.resapk"
	 */
	public boolean loadResApk(String resApk)
	{
		if (mResApkPackage != null 
				&& 0 == resApk.compareToIgnoreCase(mResApkPackage)) {
			return false;
		}
		
		if(resApk == null) return false;
		
		try {	
			// 建立資源apk包的Context
			mResApkContext = mContext.createPackageContext(resApk, 
				Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
			
			// 獲取資源apk包的Resources
			mResApkResources = mResApkContext.getResources();
			
			// 獲取資源apk包的LayoutInflater
			mResApkInflater = (LayoutInflater) mResApkContext.getSystemService(
					Context.LAYOUT_INFLATER_SERVICE);	
			
			mResApkPackage = resApk;
			
			// 使用反射機制把資源apk包中的資源名及id獲取出來
			mResIDMap = new ResIDMap(mResApkContext, mResApkPackage);					
		}
		catch (NameNotFoundException e)
		{
			e.printStackTrace();
			return false;
		}
		
		return true;
	}
	
	public class ResIDMap
	{
		public HashMap<String, Integer> mSkinIDMap = null;
		@SuppressWarnings("unused")
		private Context mContext = null;
		public ResIDMap(Context context, String skinPackage)
		{
			mContext = context;
			mSkinIDMap = getResIDMap(context, skinPackage);
		}
		
		/**
		 * 
		* Title: getResIDMap 
		* @Description:
		* @param resApkContext  the context of the resource apk, 
		* create by createPackageContext 
		* @param resApkPackagename the resource apk package name, 
		* such as com.example.resapk
		* @return
		 */
		private HashMap<String, Integer> getResIDMap(Context resApkContext, 
				String resApkPackagename)
		{
			HashMap<String, Integer> mResIDMap = new HashMap<String, Integer>();
			if (null == resApkContext || null == resApkPackagename)
			{
				return mResIDMap;
			}
			try
			{
				Class<?> RClass = resApkContext.getClassLoader().loadClass(
						resApkPackagename+".R");
				Class<?>[] cl = RClass.getClasses();
				for (int i = 0; i < cl.length; i++)
				{
					Field field[] = cl[i].getFields();
					for (int j = 0; j < field.length; j++)
					{
						if(field[j].getType().getCanonicalName().equals("int[]")){
							Log.d(TAG,"continue");
							continue;
						}
						mResIDMap.put(field[j].getName(), field[j].getInt(
								field[j].getName()));
					}
				}
			}
			catch (ClassNotFoundException e)
			{
				e.printStackTrace();
			}
			catch (IllegalArgumentException e)
			{
				e.printStackTrace();
			}
			catch (IllegalAccessException e)
			{
				e.printStackTrace();
			}
			return mResIDMap;
		}
	}	

}

在Activity中呼叫, 以下是MainAcitvity.java的程式碼:

package com.example.resapktest;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;

public class MainActivity extends Activity {
	ResLoader mResLoader;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.i("MainActivity", "ResApkTest");
		//setContentView(R.layout.activity_main);
		
		mResLoader = new ResLoader(getApplicationContext());
		
		// 載入資源apk包
		mResLoader.loadResApk("com.example.resapk");
		
		// 使用字串載入 Layout
		View activityMain = mResLoader.loadLayout("activity_main");	
		
		// 使用ID 載入Layout
		activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);
		
		if(activityMain != null) {
			setContentView(activityMain);
			View view = activityMain.findViewById(
					com.example.resapk.R.id.time_clock_2);
			view.setAlpha(0.5f);			
			Clock_setTextColor(view, 0xff0000ff);			
		}
	}
	
	public void Clock_setTextColor(View view, int color) {
		Class clockClass = view.getClass();
		try {
			Method method = clockClass.getMethod("setTextColor", int.class);			
			method.invoke(view, color);			
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}   
	}	

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle action bar item clicks here. The action bar will
		// automatically handle clicks on the Home/Up button, so long
		// as you specify a parent activity in AndroidManifest.xml.
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}
		return super.onOptionsItemSelected(item);
	}
}

五 過程優化: 1. 資源以id方式訪問:     最開始時打算將資源apk中的資源名稱及ID通過反射機制讀取出來, 記錄在map中,訪問資源時先用資源名稱獲取到ID,再通過ID獲取到真正的資源, 後來覺得這樣太影響宿主程式的編寫了, 每個資源都需要用字串來標識,從程式碼編寫和資源管理來說都相當的不科學。既然資源apk包中的資源經過編譯之後id已經固定下來並記錄到資源apk的R.java當中, 我們為什麼不能直接引用這個R.java中的id呢?  當然可以,而且不用拷貝到宿主工程中,而是直接引用資源apk中的R.java檔案, 即使用工程的Build Path -> Link Source方式,具體如下:

右擊ResApkTest工程, 選擇Build Path -> Link Source, 在彈框中選中ResApk包中的Gen目錄,把這個目錄命名為ResApk_Gen 。點選OK。

此時就是在宿主工程ResApkTest中使用ResApk中R.java中的ID了, 引用方式可以看MainAcitvity中的程式碼, 如:
// 使用ID 載入Layout
activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);
View view = activityMain.findViewById( com.example.resapk.R.id.time_clock_2);
2. 擴充套件控制元件(view)的處理:
    專案開發工程中經常會用到自定義控制元件, 因為類載入器不同, 暫時沒有在宿主工程中正常訪問ResApk中加載出來的擴充套件控制元件的方法,原因在第六點解釋,這裡給出替代方案, 也是使用類的反射機制, 這裡各個工程的依賴關係如下:


在ExtendView中有一個自定義控制元件Clock(完整程式碼請參考下載連結,此處不給出), Clock有個自己擴充套件的方法:
public void setTextColor(int color) {
        mColor = color;
 }
在ResApkTest中無法直接使用此方法,需要用反射機制來呼叫, 這裡封裝一個函式用來實現這個呼叫,程式碼如下:
public void Clock_setTextColor(View view, int color) {
        Class clockClass = view.getClass();
        try {
            Method method = clockClass.getMethod("setTextColor", int.class);            
            method.invoke(view, color);            
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }   
    }
那麼呼叫方式就是:
if(activityMain != null) {
            setContentView(activityMain);
            View view = activityMain.findViewById(
                    com.example.resapk.R.id.time_clock_2);
            view.setAlpha(0.5f);            
            Clock_setTextColor(view, 0xff0000ff);            
      }
如果有很多擴充套件方法, 那麼這樣呼叫起來會有些繁瑣, 儘量把這部分呼叫封裝起來,不要影響到客戶端程式碼的編寫。

六 遇到的問題:
1. 在使用擴充套件控制元件(view)時, 呼叫ResApk包中的擴充套件view的擴充套件方法需要用反射機制來實現, 有沒有可能像使用正常的方法一樣來使用擴充套件view的擴充套件方法呢?
以下是我做的一些嘗試,事實上,我是在嘗試失敗之後才使用反射機制來實現這個功能的。
想要正常呼叫擴充套件控制元件,那麼ResApkTest應該要對ExtendView可見, 即以某種方式引用ExtendView.jar; 以下是我嘗試的依賴關係:


修改MainActivity的onCreate函式為以下內容:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("MainActivity", "ResApkTest");
        //setContentView(R.layout.activity_main);
        
        mResLoader = new ResLoader(getApplicationContext());
        
        // 載入資源apk包
        mResLoader.loadResApk("com.example.resapk");
        
        // 使用字串載入 Layout
        View activityMain = mResLoader.loadLayout("activity_main");    
        
        // 使用ID 載入Layout
        activityMain = mResLoader.loadLayout(com.example.resapk.R.layout.activity_main);      
        View view = null;
        if(activityMain != null) {
            setContentView(activityMain);
            view = activityMain.findViewById(
                    com.example.resapk.R.id.time_clock_2);
            view.setAlpha(0.5f);            
            Clock_setTextColor(view, 0xff0000ff);            
        }        

        ClassLoader loader = getClassLoader();
        Log.i("Demo", "ResApkTest.apk 預設的類載入器 "+loader);          
          
        Log.i("Demo", "ResApkTest.apk 包中的Clock類載入器   "+Clock.class.getClassLoader());
        Log.i("Demo", "ResApk.apk 包中的Clock類載入器  "+view.getClass().getClassLoader());
        
        Log.i("Demo", "ResApkTest.apk 包中的  Clock.class hashcode "+Clock.class.hashCode());
        Log.i("Demo", "ResApk.apk 包中的  Clock.class hashcode "+view.getClass().hashCode());

        Log.i("Demo", "列印 ResApkTest 中類載入器的 parent");
        ClassLoader resApkTestLoader = Clock.class.getClassLoader();
        while (resApkTestLoader != null) {
            Log.i("Demo", "ResApkTest parent Loader  " + resApkTestLoader);
            resApkTestLoader = resApkTestLoader.getParent();
        }          

        Log.i("Demo", "列印 ResApk 中類載入器的 parent");
        ClassLoader resApkLoader = view.getClass().getClassLoader();
        while (resApkLoader != null) {
            Log.i("Demo", "ResApk parent Loader  " + resApkLoader);
            resApkLoader = resApkLoader.getParent();
        }
        
        Log.i("Demo", "view = " + view);
        Clock clock = (Clock)view;        
    }

執行MainActivity, 得到以下列印資訊和異常:
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 預設的類載入器 dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的Clock類載入器   dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的Clock類載入器  dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的  Clock.class hashcode 1093584792
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的  Clock.class hashcode 1093481360
01-01 00:28:33.429: I/Demo(2808): 列印 ResApkTest 中類載入器的 parent
01-01 00:28:33.429: I/Demo(2808): ResApkTest parent Loader  dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]
01-01 00:28:33.429: I/Demo(2808): ResApkTest parent Loader  [email protected]
01-01 00:28:33.429: I/Demo(2808): 列印 ResApk 中類載入器的 parent
01-01 00:28:33.429: I/Demo(2808): ResApk parent Loader  dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk]
01-01 00:28:33.429: I/Demo(2808): ResApk parent Loader  [email protected]
01-01 00:28:33.429: I/Demo(2808): view = [email protected]
01-01 00:28:33.429: D/AndroidRuntime(2808): Shutting down VM
01-01 00:28:33.429: W/dalvikvm(2808): threadid=1: thread exiting with uncaught exception (group=0x40c35300)
01-01 00:28:33.439: E/AndroidRuntime(2808): FATAL EXCEPTION: main
01-01 00:28:33.439: E/AndroidRuntime(2808): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.resapktest/com.example.resapktest.MainActivity}: java.lang.ClassCastException: com.example.extendview.Clock cannot be cast to com.example.extendview.Clock

開始很困惑, 為什麼com.example.extendview.Clock cannot be cast to com.example.extendview.Clock ?明明是相同的類呀。後來尋根問底,發現是這的確是兩個不同的類,它們的類物件的hash code 不一樣:
01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的  Clock.class hashcode 1093584792
01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的  Clock.class hashcode 1093481360
它們的類載入器也不一樣 01-01 00:28:33.429: I/Demo(2808): ResApkTest.apk 包中的Clock類載入器   dalvik.system.PathClassLoader[/data/app/com.example.resapktest-1.apk]01-01 00:28:33.429: I/Demo(2808): ResApk.apk 包中的Clock類載入器  dalvik.system.PathClassLoader[/data/app/com.example.resapk-2.apk] 這些都說明它們不是同一個類。
回頭看看 Context.createPackageContext 函式的註釋:

Return a new Context object for the given application name. This Context is the same as what the named application gets when it is launched, containing the same resources and class loader. Each call to this method returns a new instance of a Context object; Context objects are not shared, however they share common state (Resources, ClassLoader, etc) so the Context instance itself is fairly lightweight.

也就是說Context.createPackageContext加載出來的Context還是跟說被載入的Application相關, 跟主調的Application不一樣。

後來我嘗試了很多方法來實現用ResApkTest中的類載入器來載入ResApk中的類, 發現都不能實現需要的功能, 其中還參考了這篇關於外掛開發的博文:

http://blog.csdn.net/jiangwei0910410003/article/details/41384667

裡面有涉及類載入器的說明, 講得比較好, 讀者可以去看看。

這個問題總結一下:

    涉及資源apk和宿主apk都使用到的擴充套件View, 因為2個apk的類載入器不同,加載出來的類物件也不同,因此從apk A中加載出來的ClassA類無法與從apk B中加載出來的ClassA前置型別轉換。
    本文Demo 程式碼位於:
http://download.csdn.net/detail/romantic_energy/8793793
需要的朋友自己去下載。




相關推薦

Android apk方式共享資源動態實現方式

一 應用場景: 之前的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作量,我把這些應用的資源統一起來 一起維護,相同的資源不需要

css3動畫實現------利用長圖片資源jpg png 等實現幀動畫

首先,公司專案內部裡實現利用許多張圖片(30多張圖片)製作成一個動畫,效果是滑鼠停留時實現img的自動轉化。我的思路有2:1.js 做mouseover事件觸發處理,利用setInteval()傳入function和週期隔離事件50ms,但是在實現了相關方法之後在本地可以跑通

android初次進入,使用者引導頁蒙層效果實現

在一般app中,初次安裝使用時,除了有使用者引導圖外,還經常會看到類似於新手使用手冊的使用引導頁,類似於activity添加了一層遮罩圖。這種效果實現一般是在原activity上覆蓋一層view,可以用自定義view來實現,也可以用設計師做好和螢幕匹配的圖片後,直接全部覆蓋在

tab切換選項卡 原生/jQ/vue 實現方式

<!DOCTYPE > <html> <meta http-equiv="Content-Type" content="text/html; charset=utf

Linux將命令新增到PATH中及PA LD_LIBRARY_PATH用於指定查詢共享動態連結庫時除了預設路徑之外的其他路徑

Linux將命令新增到PATH中部落格分類:linuxLinuxApacheBash簡單說PATH就是一組路徑的字串變數,當你輸入的命令不帶任何路徑時,LINUX會在PATH記錄的路徑中查詢該命令。有的話則執行,不存在則提示命令找不到。比如在根目錄/下可以輸入命令ls,在/u

Android動態框架實現

今天介紹一下Android 中的常用的換膚策略,同時動手實現一個動態換膚的框架 先上效果圖:   換膚概念   換膚: 在android中是指 對 文字、 顏色、 圖片 等的資源的更換。 人 : 對應於現實生活中,就是我們的 膚色 、 衣服 等的

Android jar方式共享資源注意事項

最近的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作

兩種方式實現多執行緒共享資源典型的售票例子

1、繼承Thread TestThread類 public class TestThread extends Thread{ private int ticket = 300; @Override public void run() { while(true){

程式中任務中斷共享資源臨界區的保護和互斥

一、軟體法   1.輪轉法 p0 程序: while(turn != 0); //進入區 critical section ; //臨界區 turn = 1;

jenkins打包androidapk實踐經驗

首先要安裝jenkins,網上有很多教程,這裡不再囉嗦了,其次當然要安裝gradle外掛,新建一個自由風格的job,新增svn地址後配置專案中的gradle檔案 svn地址後面可以加上@HEAD,因為在使用jenkins時我遇到無法更新到最新svn程式碼的情況,比方說開發提交完程式碼我立馬就點選

Android官方文件—APP資源Handling Runtime Changes

處理執行時更改 某些裝置配置可能會在執行時更改(例如螢幕方向,鍵盤可用性和語言)。當發生這樣的更改時,Android會重新啟動正在執行的Activity(呼叫onDestroy(),然後呼叫onCreate())。重新啟動行為旨在通過使用與新裝置配置匹配的備用資源自動重新載

Android讀取assets目錄下的資源 webview載入assets下的html

1。獲取資源的輸入流 資原始檔 sample.txt 位於 $PROJECT_HOME/assets/ 目錄下,可以在 Activity 中通過 Context.getAssets().open(“sample.txt”) 方法獲取輸入流。 注意:如果資原始檔是文字檔案

如何有效的清除Android中無用的資源靜態程式碼分析

最近公司要做這個,簡單調研了一下,現有的大多數部落格也比較舊了,不太合適,總結了這麼幾個方式吧,一起來學習下。 為什麼要清除Android中這些資源呢 是這樣的,今天收到的郵件裡,有這麼一條任務: 資源優化 軟體中無用的圖片和佈局檔案,找到並驗證是否無用. 這個需要設計一套工具進行分析(自

android精簡apk大小

目的:縮小apk檔案的大小 解決方案:通過工具檢測android應用程式中未使用的資源(包括圖片甚至字串) 工具下載地址:https://code.google.com/p/android-unused-resources/ 使用方法:將下載下來的jar包放到android

Android關於小米相簿懸浮標題欄、凍結標題欄的實現方式巢狀型RecycleView

效果圖如下: 網上完全查詢不到關於凍結標題欄的實現方式,經過幾天的摸索嘗試,終於實現了這種效果;當然在過程中遇到了很多問題拖延了進度,關鍵是沒有摸清思路。 本文的實現方式已經盡了本人最大的能力進行簡化,並解決了快速滑動造成的錯亂問題,具體思路如下:

web 跨域請求共享資源OCRS)

跨域引用資源技術及其技術選型 一、源 同源策略是瀏覽器都必須遵循的策略,這就限制了js去呼叫和修改非同域下的資料。試想如果沒有這個策略,在另外一個域的js就能輕易修改當前你正在呼叫的頁面。那就天下大亂,毫無安全可言了。遵循同源策略就對非同域的資源呼叫加上了許多限制。 我們知

androidapk打簽名

從網上搜了相關的截圖,很多應用市場在召回應用的時候都會提供這個方法的,但是實際操作與圖的介紹有點相反:下圖實際上是:apkOut:空無簽名包,apkIn:上線得有簽名包。(如果還是不能成功就首先按照下圖的順序打一次包,然後再用jarsigner -verbose -keys

android之解析時出現錯誤

這次的原因不同,再記錄下public class DownloadTask { /** * @param path下載地址 * @param filePath儲存路徑 * @param progressDialog進度條 * @return * @t

ucosIII 共享資源訊號量、互斥訊號量

共享資源: 變數(靜態或全域性變數)、資料結構體、RAM表格、I/O裝置等。OS在使用一些資源時候,例如IO裝置印表機,當任務1在使用印表機時候必須保證資源獨享,避免其他任務修改列印內容導致出錯,因此需要有資源共享機制。 一般推薦使用互斥訊號量對共享資源實現

Android減小Apk大小的常用方法

我們之所以要減小apk的大小,一方面是為了節省使用者手機的記憶體;另一方面是為了節省使用者在App安裝和版本升級時的流量; 直接減小apk檔案大小的方法: 1.使用圖片壓縮工具; 目前常用的工具是: ImageOptim,壓縮效果很好,使用預設配置即可。