1. 程式人生 > >Android開發筆記(一百一十七)app省電方略

Android開發筆記(一百一十七)app省電方略

電源管理PowerManager

PowerManager是Android的電源管理類,用於管理電源操作如睡眠、喚醒、重啟以及調節螢幕亮度等等。
PowerManager的物件從系統服務POWER_SERVICE中獲取,它的主要方法如下:
goToSleep : 睡眠,即鎖屏。
wakeUp : 喚醒,即解鎖。
reboot : 重啟。
另有下列幾個隱藏的方法:
getMinimumScreenBrightnessSetting : 獲取螢幕亮度的最小值。
getMaximumScreenBrightnessSetting : 獲取螢幕亮度的最大值。
getDefaultScreenBrightnessSetting : 獲取螢幕亮度的預設值。
setBacklightBrightness : 設定螢幕亮度。


但對多數開發者來說,PowerManager在實際開發中毫無用處,因為一旦呼叫該類的方法,你的app執行時就會崩潰,檢視日誌報錯“java.lang.SecurityException: Neither user 10150 nor current process has android.permission.DEVICE_POWER.”這個錯誤資訊倒是容易看懂,好吧,那我便在AndroidManifest.xml中加上DEVICE_POWER的許可權。可是加了許可權之後,ADT又提示錯誤“Permission is only granted to system apps”。這下傻眼了,怎麼會說“許可權只授予系統應用程式”呢?不過這難不倒我,咱把app工程clean一下,錯誤提示就不見了,然後重新Run之,結果Console欄出現紅色文字“Installation error: INSTALL_FAILED_SHARED_USER_INCOMPATIBLE”,還是不行呀。
找了大量的資料,才發現這是因為電源管理的許可權,只有系統程式(打了系統簽名)才可以獲得,使用者程式無法獲取這個許可權。大夥對該問題基本是束手無策,只有Stack Overflow上的大神給了個解決方案,主要做三方面的修改:
1、在AndroidManifest.xml中加上DEVICE_POWER、REBOOT、SHUTDOWN的許可權。
2、在AndroidManifest.xml的manifest節點中增加屬性說明“android:sharedUserId="android.uid.system"”,這表示使用系統使用者的uid。
3、為了能夠共享系統使用者的uid,你的app得采用系統簽名打包,即先找到目標Android系統的platform.pk8和platform.x509.pem金鑰檔案,然後使用signapk.jar將apk簽名到指定金鑰。
這個解決方案理論上可行,但就真機來說,每個品牌每個型號的手機,其系統簽名都是不一樣的。因此,就算你真的搞出來一個系統應用,那也僅適用於該簽名版本的Android系統,而不能用於其他簽名的Android系統,所以PowerManager只能是手機廠商內部使用了。


下面是PowerManager幾個用途的示例程式碼(一般用不到,僅供參考):
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;

//注意,PowerManager只有系統應用才能操作,普通應用不能操作,所以下面程式碼僅供參考
public class PowerUtil {
	
	private final static String TAG = "PowerUtil";
	
	private static int getValue(Context ctx, String methodName, int defValue) {
		int value = defValue;
		PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
		try {
			Class<?> pmClass = Class.forName(pm.getClass().getName());
			Field field = pmClass.getDeclaredField("mService");
			field.setAccessible(true);
			Object iPM = field.get(pm);
			Class<?> iPMClass = Class.forName(iPM.getClass().getName());
			Method method = iPMClass.getDeclaredMethod(methodName);
			method.setAccessible(true);
			value = (Integer) method.invoke(iPM);
		} catch (Exception e) {
			e.printStackTrace();
		}
		Log.d(TAG, "methodName="+methodName+", value="+value);
		return value;
	}

	public static int getMinLight(Context ctx) {
		return getValue(ctx, "getMinimumScreenBrightnessSetting", 0);
	}

	public static int getMaxLight(Context ctx) {
		return getValue(ctx, "getMaximumScreenBrightnessSetting", 255);
	}

	public static int getDefLight(Context ctx) {
		return getValue(ctx, "getDefaultScreenBrightnessSetting", 100);
	}
	
    //設定螢幕亮度。light取值0-255
    public static void setLight(Context ctx, int light) {
		PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
        try {
            Class<?> pmClass = Class.forName(pm.getClass().getName());
            // 得到PowerManager類中的成員mService(mService為PowerManagerService型別)
            Field field = pmClass.getDeclaredField("mService");
            field.setAccessible(true);
            // 例項化mService
            Object iPM = field.get(pm);
            // 得到PowerManagerService對應的Class物件
            Class<?> iPMClass = Class.forName(iPM.getClass().getName());
            /*
             * 得到PowerManagerService的函式setBacklightBrightness對應的Method物件,
             * PowerManager的函式setBacklightBrightness實現在PowerManagerService中
             */
            Method method = iPMClass.getDeclaredMethod("setBacklightBrightness", int.class);
            method.setAccessible(true);
            // 呼叫實現PowerManagerService的setBacklightBrightness
            method.invoke(iPM, light);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void resetLight(Context ctx, int light) {
        try {
            Object power;
            Class <?> ServiceManager = Class.forName("android.os.ServiceManager");
            Class <?> Stub = Class.forName("android.os.IPowerManager$Stub");

            Method getService = ServiceManager.getMethod("getService", new Class[] {String.class});
            //Method asInterface = GetStub.getMethod("asInterface", new Class[] {IBinder.class});//of this class?
            Method asInterface = Stub.getMethod("asInterface", new Class[] {IBinder.class});    //of this class?
            IBinder iBinder = (IBinder) getService.invoke(null, new Object[] {Context.POWER_SERVICE});//
            power = asInterface.invoke(null,iBinder);//or call constructor Stub?//

            Method setBacklightBrightness = power.getClass().getMethod("setBacklightBrightness", new Class[]{int.class}); 
            setBacklightBrightness.invoke(power, new Object[]{light});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    //鎖屏
    public static void lockScreen(Context ctx) {
		PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
        pm.goToSleep(SystemClock.uptimeMillis());
    }

    //解鎖
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
	public static void unLockScreen(Context ctx) {
		PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
        pm.wakeUp(SystemClock.uptimeMillis());
    }

    //重啟
    public static void reboot(Context ctx) {
		PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
        pm.reboot(null);
    }

    //關機
    public static void shutDown(Context ctx) {
        Intent intent = new Intent("android.intent.action.ACTION_REQUEST_SHUTDOWN");
        intent.putExtra("android.intent.extra.KEY_CONFIRM", false);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        // 彈出系統內建的對話方塊,選擇確定關機或取消關機
        ctx.startActivity(intent);
    }
    
}


電池管理BatteryManager

BatteryManager名為電池管理,然而檢視該類的原始碼,裡面只有一些常量定義,並非真正意義上的電池管理。事實上,開發者並不能直接管理電池,要想獲取電池的相關資訊,得通過監聽電量改變事件來得知。


電池的電量改變事件,其動作名稱是Intent.ACTION_BATTERY_CHANGED,因為接受該事件要求app必須處於活動狀態,所以用來監聽的廣播接收器不能在AndroidManifest.xml中靜態註冊,而只能在app程式碼中通過registerReceiver方法來動態註冊。下面是電量改變事件中攜帶的引數資訊:
BatteryManager.EXTRA_SCALE : 電量刻度,通過getIntExtra獲取。通常是100
BatteryManager.EXTRA_LEVEL : 當前電量,通過getIntExtra獲取。
BatteryManager.EXTRA_STATUS : 當前狀態,通過getIntExtra獲取。
--BATTERY_STATUS_UNKNOWN = 1; 表示未知
--BATTERY_STATUS_CHARGING = 2; 表示正在充電
--BATTERY_STATUS_DISCHARGING = 3; 表示正在斷電
--BATTERY_STATUS_NOT_CHARGING = 4; 表示不在充電
--BATTERY_STATUS_FULL = 5; 表示充滿
BatteryManager.EXTRA_HEALTH : 健康程度,通過getIntExtra獲取。
--BATTERY_HEALTH_UNKNOWN = 1; 表示未知
--BATTERY_HEALTH_GOOD = 2; 表示良好
--BATTERY_HEALTH_OVERHEAT = 3; 表示過熱
--BATTERY_HEALTH_DEAD = 4; 表示壞了
--BATTERY_HEALTH_OVER_VOLTAGE = 5; 表示短路
--BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6; 表示未知錯誤
--BATTERY_HEALTH_COLD = 7; 表示冷卻
BatteryManager.EXTRA_VOLTAGE : 當前電壓,通過getIntExtra獲取。
BatteryManager.EXTRA_PLUGGED : 當前電源,通過getIntExtra獲取。
--0 表示電池
--BATTERY_PLUGGED_AC = 1; 表示充電器
--BATTERY_PLUGGED_USB = 2; 表示USB
--BATTERY_PLUGGED_WIRELESS = 4; 表示無線
BatteryManager.EXTRA_TECHNOLOGY : 當前技術,通過getStringExtra獲取。比如返回Li-ion表示鋰電池。
BatteryManager.EXTRA_TEMPERATURE : 當前溫度,通過getIntExtra獲取。
BatteryManager.EXTRA_PRESENT : 是否提供電池,通過getBooleanExtra獲取。


除了電量改變事件,還有幾個事件與電池有關,如下所示
Intent.ACTION_BATTERY_LOW : 電池電量過低,靜態註冊時使用android.intent.action.BATTERY_LOW
Intent.ACTION_BATTERY_OKAY : 電池電量恢復,靜態註冊時使用android.intent.action.BATTERY_OKAY
Intent.ACTION_POWER_CONNECTED : 連上外部電源,靜態註冊時使用android.intent.action.ACTION_POWER_CONNECTED
Intent.ACTION_POWER_DISCONNECTED : 斷開外部電源,靜態註冊時使用android.intent.action.ACTION_POWER_DISCONNECTED


下面是電池事件的監聽截圖:



下面是監聽電池事件的程式碼示例:
import com.example.exmbattery.util.DateUtils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.widget.TextView;

public class BatteryActivity extends Activity {

	private TextView tv_battery_change;
	private static TextView tv_power_status;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_battery);

		tv_battery_change = (TextView) findViewById(R.id.tv_battery_change);
		tv_power_status = (TextView) findViewById(R.id.tv_power_status);
	}
	
	@Override
	protected void onStart() {
		super.onStart();
		batteryChangeReceiver = new BatteryChangeReceiver();
		IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
		registerReceiver(batteryChangeReceiver, filter);
	}
	
	@Override
	protected void onStop() {
		super.onStop();
		unregisterReceiver(batteryChangeReceiver);
	}

    private BatteryChangeReceiver batteryChangeReceiver;
    private class BatteryChangeReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent != null) {
            	int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
            	int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
            	int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0);
            	int healthy = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0);
            	int voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0);
            	int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 3);
            	String technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY);
            	int temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
            	boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);

            	String desc = String.format("%s : 收到廣播:%s",
            			DateUtils.getNowDateTime(), intent.getAction());
            	desc = String.format("%s\n電量刻度=%d", desc, scale);
            	desc = String.format("%s\n當前電量=%d", desc, level);
            	desc = String.format("%s\n當前狀態=%s", desc, mStatus[status]);
            	desc = String.format("%s\n健康程度=%s", desc, mHealthy[healthy]);
            	desc = String.format("%s\n當前電壓=%d", desc, voltage);
            	desc = String.format("%s\n當前電源=%s", desc, mPlugged[plugged]);
            	desc = String.format("%s\n當前技術=%s", desc, technology);
            	desc = String.format("%s\n當前溫度=%d", desc, temperature/10);
            	desc = String.format("%s\n是否提供電池=%s", desc, present?"是":"否");
            	tv_battery_change.setText(desc);
            }
        }
    }
    
    private static String[] mStatus = {"不存在", "未知", "正在充電", "正在斷電", "不在充電", "充滿"};
    private static String[] mHealthy = {"不存在", "未知", "良好", "過熱", "壞了", "短路", "未知錯誤", "冷卻"};
    private static String[] mPlugged = {"電池", "充電器", "USB", "不存在", "無線"};

    private static String mChange = ""; 
    public static class PowerChangeReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent != null) {
            	mChange = String.format("%s\n%s : 收到廣播:%s", 
            			mChange, DateUtils.getNowDateTime(), intent.getAction());
            	tv_power_status.setText(mChange);
            }
        }
    }
}


省電方法/螢幕開關事件

前面說了許多廢話,趕快回到本文的主題——省電。app開發與伺服器程式開發不同,app所在的移動裝置是很缺電的,幾天就要充一次電,所以如果你的app特別耗電,一天甚至半天就把使用者手機搞沒電了,那麼通常逃脫不了被解除安裝的悲慘命運。因此,為人為己,開發者還是儘可能讓app執行的時候省電些,綠色環保的低碳生活,從開發app做起。


然而目前尚無法檢測每個應用的耗電程度,一般是靠經驗判斷,基本原則就是:越消耗資源的,耗電就越大。具體到程式碼編寫,主要有以下省電措施:
1、能用整型數計算,就不用浮點數計算。
2、能用json解析,就不用xml解析。
3、能用網路定位,就不用GPS定位。
4、儘量減少大檔案的下載(如先壓縮再下載,或者快取已下載的檔案)。
5、用完系統資源,要及時回收。佔著茅坑不拉屎,使用者手機會很蛋疼。相關例子參見《Android開發筆記(七十五)記憶體洩漏的處理
6、能用執行緒處理,就不用程序處理。
7、多用快取複用物件資源。如螢幕尺寸只需獲取一次,其後可到快取中讀取,全域性變數技術參見《Android開發筆記(二十八)利用Application實現記憶體讀寫》。相關例子還可參見《Android開發筆記(七十六)執行緒池管理》、《Android開發筆記(七十七)圖片快取演算法
8、能用定時器廣播,就不用後臺常駐服務。
9、能用記憶體儲存,就不用檔案儲存。


省電措施雖多,那要如何得知省電效果呢?在實際開發中,耗電大戶其實是在後臺默默執行的Service服務,想想看,手機待機的時候,螢幕都不亮了,可是手機裡面還有一些不知疲倦的Service在愚公移山,愚公也是要吃飯的呀。我做過實驗,一個app在系統待機時仍然滿血Service執行,一小時後手機電量消耗4%;同一個app改造後在系統待機時不執行任何Service,一小時後手機電量消耗2%;一小時相差2%,十小時便相差20%啊,原來我們手機的電量就是這樣被一點一點耗光的。


既然如此,我們若想避免app在手機待機時仍在做無用功,就要在螢幕關閉時結束指定任務,在螢幕點亮時再開始指定任務。這裡用到了下面三個螢幕開關事件:
Intent.ACTION_SCREEN_ON : 螢幕點亮事件
Intent.ACTION_SCREEN_OFF : 螢幕關閉事件
Intent.ACTION_USER_PRESENT : 使用者解鎖事件,靜態註冊時使用android.intent.action.USER_PRESENT
使用上述三個事件要注意幾點:
1、螢幕點亮事件和螢幕關閉事件必須在程式碼中動態註冊。如果在AndroidManifest.xml中靜態註冊,則不起任何作用。
2、在關閉螢幕時,系統先暫停所有活動頁面,然後才關閉螢幕;同樣的,在點亮螢幕時,系統點亮螢幕,然後才恢復活動頁面。所以這幾個事件不能在Activity中註冊/登出,只能在自定義Application的onCreate方法中註冊,在onTerminate方法中登出。
3、Activity要想獲取螢幕開關事件,得通過自定義的Application類去間接獲取。


下面是螢幕開關事件的捕捉截圖:



下面是螢幕開關事件的程式碼:
import com.example.exmbattery.util.DateUtils;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class LockScreenReceiver extends BroadcastReceiver {

	private static final String TAG = "LockScreenReceiver";

	@Override
	public void onReceive(Context context, Intent intent) {
		if (intent != null) {
			String mChange = "";
			mChange = String.format("%s\n%s : 收到廣播:%s", mChange,
					DateUtils.getNowDateTime(), intent.getAction());
			if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
				mChange = String.format("%s\n這是螢幕點亮事件", mChange);
			} else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
				mChange = String.format("%s\n這是螢幕關閉事件", mChange);
			} else if (intent.getAction().equals(Intent.ACTION_USER_PRESENT)) {
				mChange = String.format("%s\n這是使用者解鎖事件", mChange);
			}
			Log.d(TAG, mChange);
			MainApplication.getInstance().setChangeDesc(mChange);
		}
	}

}


下面是自定義Application的程式碼:
import android.app.Application;
import android.content.Intent;
import android.content.IntentFilter;

public class MainApplication extends Application {

	private static MainApplication mApp;
	private LockScreenReceiver mReceiver;
	private String mChange = "";

	public static MainApplication getInstance() {
		return mApp;
	}

	public String getChangeDesc() {
		return mApp.mChange;
	}

	public void setChangeDesc(String change) {
		mApp.mChange = mApp.mChange + change;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		mApp = this;
		mReceiver = new LockScreenReceiver();
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_SCREEN_ON);
		filter.addAction(Intent.ACTION_SCREEN_OFF);
		filter.addAction(Intent.ACTION_USER_PRESENT);
		registerReceiver(mReceiver, filter);
	}
	
	@Override
	public void onTerminate() {
		unregisterReceiver(mReceiver);
		super.onTerminate();
	}
	
}


下面是顯示螢幕開關事件的頁面程式碼
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class ScreenActivity extends Activity {

	private static TextView tv_screen;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_screen);
		tv_screen = (TextView) findViewById(R.id.tv_screen);
	}

	@Override
	protected void onStart() {
		super.onStart();
		tv_screen.setText(MainApplication.getInstance().getChangeDesc());
	}

}







點此檢視Android開發筆記的完整目錄