其中PowerTutor并不通用,只实现了3中机型:G1、G2和Nexus One,其余的机型都归为Unknown。
代码主要分为6个包:components,phone,service,ui,util,widget
。每个包下都有自己的类实现了相应的功能。
补充:众所周知,Pid是进程ID,Uid是用户ID,只是Android和计算机不一样,计算机每个用户都具有一个Uid,哪个用户start的程序,这个程序的Uid就是那个那个用户,而Android中每个程序都有一个Uid,默认情况下,Android会给每个程序分配一个普通级别互不相同的 Uid,如果用互相调用,只能是Uid相同才行,这就使得共享数据具有了一定安全性,每个软件之间是不能随意获得数据的。而同一个application 只有一个Uid,所以application下的Activity之间不存在访问权限的问题。
主要功能
给出每一种型号的手机的特定参数,主要由接口PhoneConstants
负责,不同型号的手机都直接或者间接实现了这个接口(DreamConstants、PassionConstants、SapphireConstants)
。
根据包components中传来的每种硬部件的数据,计算手机中每一种硬部件的Power,主要由接口
PhonePowerCalculator
负责,不同型号的手机直接或者间接实现了这个接口,如:
(DreamPowerCalculator、PassionPowerCalculator、SapphirePowerCalculator)。
PhoneSelector
类。根据手机硬件信息,通过方法getPhoneType()
获取相应的手机型号。在方法
generateComponents(Context context,List<PowerComponent> components,List<PowerFunction> functions)
中,根据1和2给出的对应的手机型号的参数和计算Power方法,实现了传递过来的components
(就是一些参数)和functions
(计算机各种硬部件Power的函数)。包service
下的PowerEstimator
类调用了这个方法。
注意:由于目前主流的显示屏幕主要使用的是LCD和OLED两种技术,但是这两种技术在计算屏幕能耗的时候用的方法相差很大,所以在前面的一些步骤中需要先判断一下手机屏幕使用的是哪种技术。
整个应用的所有用户界面都跟在这个包中。其中类UMLogger是整个应用的主界面,其对应的布局文件是R.layout.main。点击“Application Viewer”按钮后,进入的界面主要由PowerTop类负责,其使用的是动态布局文件。在主界面点击“System Viewer”按钮后,在顶部有3个tabs,整个父控件主要类PowerTabs负责,对应的布局文件是:R.layout.power_tabs。其中
其中的类StartupReceiver是一个广播类,当手机开机时,它能接受开机广播,判断应用程序的服务要不要一起启动。
注意:这一部内容居然使用了
android AIDL( Android Interface definition language)技术
在android项目的gen目录下有一个类:ICounterService
,继承了android.os.IInterface
。它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口。这种技术之前从未接触过,需要找时间进行学习一下。
这个包中包含了最主要的几个耗能的android手机硬部件(Audio,CPU,GPS,LCD,OLED,Wifi,3G,sensor?)
。其中每个硬部件的类都继承了PowerComponent类(PowerComponent类继承了Thread)
,所以所有硬部件的类也是一个线程。其中每个硬部件都有自己的数据形式(如:CpuData、GpsData、LcdData、OledData、ThreegData、WifiData
,每种数据形式代表着各种硬部件的特点),他们均继承PowerData
,除了自己的属性外,都有一个回收类负责回收不需要的数据,减少占用的内存;最后还负责将数据写入日志文件。
每个硬部件的类都复写了方法:
public IterationData calculateIteration(long iteration);
这个方法收集计算每个硬部件的相关数据,最后返回IterationData
格式的数据,包含2个元素:使用该硬部件的程序和耗能的多少。
在计算cpu产生的相关能耗中,分为两个部分进行了计算:用户态(user) 与 核心态(system)
变量
CPU_FREQ_FILE=/proc/cpuinfo
STAT_FILE=/proc/stat
分别存储着cpu的频率和状态。
private double readCpuFreq(SystemInfo sysInfo)
方法负责读取cpu的频率,首先从
“/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"
下读取,如果读取失败,则从目录
“/proc/cpuinfo“
读取,该文件中紧跟在“BogoMIPS”后数据即为cpu频率。(但是这个数据在台式机上跟cpu的频率好像相差好大啊!)
private SparseArray<CpuStateKeeper> pidStates;
private SparseArray<CpuStateKeeper> uidLinks; 这两个变量用来干什么的?且类CpuStateKeeper的作用是什么?
public double getUsrPerc() {
return 100.0 * sumUsr / Math.max(sumUsr + sumSys, deltaTotal);
}
public double getSysPerc() {
return 100.0 * sumSys / Math.max(sumUsr + sumSys, deltaTotal);
}
这两个方法分别计算在整个应用程序的执行过程中,用户态下和核心态下所用CPU时间占总CPU时间的比率。
在CPU.java
中,方法:
public IterationData calculateIteration(long iteration){}
是其中最核心的代码,负责收集相关数据,最后使用变量result分别存储了:
此外,该类包含了一个内部类CpuStateKeeper
,用来更新获取判断cpu的相关数据和相关状态。在方法
public IterationData calculateIteration(long iteration){}
中,首先,使用方法readCpuFreq(sysInfo)
读取cpu的频率,使用方法sysInfo.getUsrSysTotalTime(statsBuf)
,将整个cpu的运行时间(包括userTime与systemTime
)都读取到变量statsBuf[]
中。变量cpuState
存储的是该应用程序的使用的cpu的整个能耗。然后
data.init(cpuState.getUsrPerc(), cpuState.getSysPerc(), freq);
result.setPowerData(data);
这两行代码负责将整个的cpu能耗存储到result中。
注意下面两个目录或者文件:
/proc/stat :存储了整个系统的user total time 以及 system total time
/proc/" + pid + "/stat :存储了当前 pid 的 user time 以及 system time
最后,result再分别将各个uid的能耗也存入到result中,然后返回。
for(int i = 0; i < uidLinks.size(); i++) {
int uid = uidLinks.keyAt(i);
CpuStateKeeper linkState = uidLinks.valueAt(i);
CpuData uidData = CpuData.obtain();
predictAppUidState(uidData, linkState.getUsrPerc(),
linkState.getSysPerc(), freq);
result.addUidPowerData(uid, uidData);
}
private static final String[] BACKLIGHT_BRIGHTNESS_FILES = {
"/sys/devices/virtual/leds/lcd-backlight/brightness",
"/sys/devices/platform/trout-backlight.0/leds/lcd-backlight/brightness",
};
在这两个系统文件中可以读取屏幕亮度的值,如果系统没有这两个文件(我的手机HTC—t328w,就找不到这两个文件),那么就调用API获取:
brightness = Settings.System.getInt(context.getContentResolver(),Settings.System.SCREEN_BRIGHTNESS);
程序中使用了util包中的类ForegroundDetector.java
来进行判断正在使用屏幕的应用程序是哪一个,且负责监听使用屏幕的程序的切换。最后使用方法
public IterationData calculateIteration(long iteration)
将使用屏幕的程序和屏幕的亮度级别的数据收集返回。方法中的的核心代码:
if(screen) {
LcdData uidData = LcdData.obtain();
uidData.init(brightness, screen);
result.addUidPowerData(foregroundDetector.getForegroundUid(), uidData); //(result为IterationData类型)
}
实例化一个LcdData
的对象uidData
,然后用屏幕的亮度级别和是否屏幕被使用(brightness, screen)
初始化uidData
。最后result使用方法:
public void addUidPowerData(int uid, PowerData power)
将程序uid和使用的data保存起来后返回。
如同CPU.java
一样,其包含了一个内部类:WifiStateKeeper
,进行对wifi的数据状态等一些属性进行操作处理的类。
Wifi.java
也有自己的数据格式 WifiData
。在Wifi的构造函数中,获取了wifi传输的接口,然后给一些变量赋值,包括存放传输/读取的packets
以及传输/读取的Bytes
的文件目录。代码如下:
String interfaceName = SystemInfo.getInstance().getProperty("wifi.interface");
transPacketsFile = "/sys/devices/virtual/net/" +interfaceName + "/statistics/tx_packets";
readPacketsFile = "/sys/devices/virtual/net/" + interfaceName + "/statistics/rx_packets";
transBytesFile = "/sys/devices/virtual/net/" + interfaceName + "/statistics/tx_bytes";
readBytesFile = "/sys/devices/virtual/net/" + interfaceName + "/statistics/rx_bytes";
uidStatsFolder = new File("/proc/uid_stat");
同样,所有关于wifi的使用数据的收集和计算处理,都在放在了方法
public IterationData calculateIteration(long iteration){};
在该方法中,首先,我们判断wifi的装状态,如果wifi已经关闭或者正在开启,我们都对之前的存的数据清空,以便下回使用内存。如果在别的状态下(如:wifi已经开启或者正处于要关闭的状态),将执行以下的操作:
从目录文件中读取所有相关的数据(传输/接受的bytes
,传输/接受的packets
),如果数据读取失败则返回。每隔15
秒,获取一次wifi
的Linkspeed
。判断wifi
状态是不是已被初始化过(即是否启动过),如果是,则先进行更新数据,然后进行各种计算,实例化数据,最后把总共消耗的能量数据存储到result
,等到最后进行返回。
然后我们要获取每个uid对应的wifiData。首先我们用方法
lastUids = sysInfo.getUids(lastUids);
获取所有的uid。然后对每个uid
进行循环,把相对应的wifiData
存入到result
中,最后一起返回。在这个过程中,我们首先要从文件中读取每个uid
对应的接收/传输的bytes
receiveBytes = sysInfo.readLongFromFile("/proc/uid_stat/" + uid + "/tcp_rcv");
transmitBytes = sysInfo.readLongFromFile("/proc/uid_stat/" + uid + "/tcp_snd");
在计算每个uid对应的wifiData时,
for(int uid : lastUids){
.................................
WifiStateKeeper uidState = uidStates.get(uid);
...................................
receiveBytes = sysInfo.readLongFromFile("/proc/uid_stat/" + uid + "/tcp_rcv");
transmitBytes = sysInfo.readLongFromFile("/proc/uid_stat/" + uid + "/tcp_snd");
..................................
long deltaTransmitBytes = transmitBytes - uidState.getTransmitBytes();
long deltaReceiveBytes = receiveBytes - uidState.getReceiveBytes();
}
对获取的uid进行循环,为了获取当前uid
的Bytes
数据,获取了一个当前的uidState
,用当前获取的总共的Bytes
减去当前uid
上一次的
Bytes(long deltaTransmitBytes = transmitBytes - uidState.getTransmitBytes())。
这边有一个问题:我们如果想获得当前uid
的bytes
,方程应该为:当前获得的总共Bytes减去从开启wifi到上一次传输和接受的所有Bytes的总和。
但是计算的时候使用的Packets作为参数,所以需要将Bytes
转换成Packets
。
最后如果wifi是active
的,则存储数据到result
中
if(active) {
WifiData uidData = WifiData.obtain();
uidData.init(uidState.getPackets(), uidState.getUplinkBytes(),
uidState.getDownlinkBytes(), uidState.getUplinkRate(),
linkSpeed, uidState.getPowerState());
result.addUidPowerData(uid, uidData);
}
同理,Audio中也还有有一个内部类AudioData,继承了PowerData,负责Audio引起的相关能耗的数据。此外,Audio还有自己独特的一个数据类:MediaData类,实现了Comparable接口。
1、MeidaData,拿来干什么用 2、Audio的数据从哪里获得 3、为什么Audio的构造函数里会有别的方法。
这个类主要负责开始记录和收集各个部件生成的各种信息。这些信息会被记录到日志文件中,然后传到服务器。同时,类中也实现了ICounterService IPC
接口。
其中字符串常量
private static final String DEFLATE_DICTIONARY
是用来帮助压缩上传的日志文件的。在构造函数中实例化了一些变量,uidAppIds
是map
类型,key
对应的是程序的uid
,value
对应的是程序的packetname@versioncode
。其中powerFunctions
是计算每一个硬部件的方法。histories
是一个集合类型,存储了每个硬部件的一段时间内的数据。方法openLog()
用来记录日志和上传日志文件的。
run()
方法是整个类的核心代码,用来更新整个应用程序的到的power
数据的。
for(int i = 0; i < components; i++) {
powerComponents.get(i).init(beginTime, ITERATION_INTERVAL);
powerComponents.get(i).start();
}
beginTime
是指从系统开机到当前的时间,ITERATION_INTERVAL
是指间隔。我们对部件的个数进行循环,初始化没一个硬部件,然后启动硬部件的线程(注意:我们的每一个硬部件都继承了PowerComponent
,由于PowerComponent
继承了线程Thread
,所以,每个硬部件都是一个线程)。
PhoneConstants phoneConstants = PhoneSelector.getConstants(context);
负责选择是哪一款型号的手机,且返回该型号手机的phoneConstants
。随后我们判断该型号的手机的屏幕使用的技术是否为“OLED”
。然后不停的循环,每隔一个iteration
收集一次各个硬部件的能耗数据。
PowerComponent comp = powerComponents.get(i);
IterationData data = comp.getData(iter);
在其中的for
循环中,我们首先取得每个硬部件的能耗数据存储到IterationData
类型的data
中。
随后,更加关键的代码如下:
SparseArray<PowerData> uidPower = data.getUidPowerData();
for(int j = 0; j < uidPower.size(); j++) {
int uid = uidPower.keyAt(j);
PowerData powerData = uidPower.valueAt(j);
int power = (int)powerFunctions.get(i).calculate(powerData);
powerData.setCachedPower(power);
histories.get(i).add(uid, iter, power);
if(uid == SystemInfo.AID_ALL) {
totalPower += power;
}
if(i == oledId) {
OLED.OledData oledData = (OLED.OledData)powerData;
if(oledData.pixPower >= 0) {
oledScoreHistory.add(uid, iter, (int)(1000 * oledData.pixPower));
}
}
}
我们先从data
中得到PowerData
类型的数据放到uidPower
中,最后我们得到每个uidPower
对应的uid
,然后计算得到最终的能耗power
,然后把它与对应的uid
存入到histories
中。如果
uid==SystemInfo.AID_ALL
即,对应的不是单个应用程序的能耗,而是整个手机在这个部件消耗的能耗,我们则将其加入到整个能耗中:
totalPower += power;
最后,如果该手机的屏幕使用的是oled
的,那么把其相关power
数据加入到历史数据中。
在该方法中,剩下的代码主要是将相关日志信息记录到日志文件PowerTrace.log
中。然后每隔15秒更新一下通知栏的数据:
context.updateNotification((int)Math.min(8, 1 +8 * avgPower / phoneConstants.maxPower()),avgPower);
每隔60
秒,更新一下widget
的数据:
if(iter % 60 == 0) {
PowerWidget.updateWidget(context, this);
}
这个类主要是保存历史能耗数据的一个buffer
。主要是这个类中的成员变量有点复杂。首先,包含了两个内部类:UidData
和 HistoryDatum
,这两个类的作用类似于两个数据结构,除了构造函数和数据初始化,就没有别的函数了。最后UidData是主要的数据结构,内含了HistoryDatum
的队列,以及两个Counter
类型(util
包下的Count.java
)的变量。最后HistoryBuffer
自己定义了一个
private SparseArray<UidData> uidData; //其中以UidData作为存储的类型。
主要方法:
public synchronized void add(int uid, long iteration, int power) {};
参数:
int uid , 代表应用程序的uid,用于识别每个应用程序或者total(uid = -1)
long iteration ,指时间,代表这个是第几秒的时间。
int power , 该uid使用的能耗数据。
该方法的作用主要有两个:
data.sum.add(power)
; 把该uid
对应的power
的总和保存起来,存入到data.sum
中 ;queue.addFirst(datum)
; 把该uid
对应的power
数据按时间顺序(iteration
参数)加入到队列queue
中。