Android開發筆記(一百一十七)app省電方略
阿新 • • 發佈:2019-01-30
電源管理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開發筆記的完整目錄