四、功耗优化案例分析
4.1 CPU异常SOP
4.1.1 CPU异常归因
CPU高负载异常是最常见的一类功耗问题。引起CPU高负载的原因有很多,通常是业务开发的不规范导 致的。高功耗任务主要通过线下和线上的工具,识别出CPU异常,进行进行Case by case的优化。
除了使用Android Profiler工具之外,也可以使用代码来实现CPU使用率的监控。以下是一个简单的示例代码,可以在应用程序中实现CPU使用率的实时监控:
private void startCpuMonitor() { final Handler handler = new Handler(); final Runnable runnable = new Runnable() { @Override public void run() { // 获取CPU使用率 float cpuUsage = getCpuUsage(); // 处理CPU使用率 handleCpuUsage(cpuUsage); // 间隔1秒钟再次执行 handler.postDelayed(this, 1000); } }; handler.postDelayed(runnable, 1000); } private float getCpuUsage() { try { RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r"); String line = reader.readLine(); String[] fields = line.trim().split("\s+"); long totalCpuTime1 = Long.parseLong(fields[1]) + Long.parseLong(fields[2]) + Long.parseLong(fields[3]) + Long.parseLong(fields[4]) + Long.parseLong(fields[5]) + Long.parseLong(fields[6]) + Long.parseLong(fields[7]); long idleCpuTime1 = Long.parseLong(fields[4]); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } reader.seek(0); line = reader.readLine(); reader.close(); fields = line.trim().split("\s+"); long totalCpuTime2 = Long.parseLong(fields[1]) + Long.parseLong(fields[2]) + Long.parseLong(fields[3]) + Long.parseLong(fields[4]) + Long.parseLong(fields[5]) + Long.parseLong(fields[6]) + Long.parseLong(fields[7]); long idleCpuTime2 = Long.parseLong(fields[4]); return (totalCpuTime2 - totalCpuTime1) * 100.0f / (totalCpuTime2 - totalCpuTime1 + idleCpuTime2 - idleCpuTime1); } catch (IOException e) { e.printStackTrace(); } return 0.0f; } private void handleCpuUsage(float cpuUsage) { // 处理CPU使用率 // ... }
以上代码在每隔1秒钟获取一次CPU使用率数据,并调用handleCpuUsage
方法来处理CPU使用率。可以根据具体需求,来修改代码中的处理逻辑。
在Android操作系统中,可以通过代码的方式获取CPU使用率数据,并根据数据来确定异常CPU使用率的阈值。下面是一个简单的示例代码:
// 获取CPU使用率 private float getCpuUsage() { try { RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r"); String line = reader.readLine(); String[] fields = line.trim().split("\s+"); long totalCpuTime1 = Long.parseLong(fields[1]) + Long.parseLong(fields[2]) + Long.parseLong(fields[3]) + Long.parseLong(fields[4]) + Long.parseLong(fields[5]) + Long.parseLong(fields[6]) + Long.parseLong(fields[7]); long idleCpuTime1 = Long.parseLong(fields[4]); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } reader.seek(0); line = reader.readLine(); reader.close(); fields = line.trim().split("\s+"); long totalCpuTime2 = Long.parseLong(fields[1]) + Long.parseLong(fields[2]) + Long.parseLong(fields[3]) + Long.parseLong(fields[4]) + Long.parseLong(fields[5]) + Long.parseLong(fields[6]) + Long.parseLong(fields[7]); long idleCpuTime2 = Long.parseLong(fields[4]); return (totalCpuTime2 - totalCpuTime1) * 100.0f / (totalCpuTime2 - totalCpuTime1 + idleCpuTime2 - idleCpuTime1); } catch (IOException e) { e.printStackTrace(); } return 0.0f; } // 判断CPU使用率是否异常 private boolean isCpuUsageAbnormal(float cpuUsage, float threshold) { return cpuUsage > threshold; }
以上代码通过读取/proc/stat
文件获取CPU使用率数据,然后计算CPU使用率,并根据预设的阈值来判断CPU使用率是否异常。这里假设异常阈值为80%。
// 获取CPU使用率 float cpuUsage = getCpuUsage(); // 判断CPU使用率是否异常 if (isCpuUsageAbnormal(cpuUsage, 80.0f)) { // CPU使用率异常处理 // ... }
注意,实际使用时需要考虑到多种因素,如不同的设备、不同的Android版本等,可能需要做一定的兼容性处理。
4.1.2 CPU异常治理
4.1.2.1 死循环
Bad Case
死循环类:循环退出条件达不到
handler消息循环
// 边界条件未满足,无法break while (true) { ... if (shouldExit()) { break } } // 异常处理不妥当,导致死循环 while (true) { try { do someting; break; } catch (e) { } } // 消息处理不当,导致Handler线程死循环 void handleMessage(Message msg) { //do something handler.sendEmptyMessage(MSG) }
异常分支处理不当
// 方法逻辑有裁剪,仅贴出主要逻辑 private JSONArray packMiscLog() { do { ...... try { cursor = mDb.query(......); int n = cursor.getCount(); ...... if (start_id >= max_id) { break; } } catch (Exception e) { } finally { safeCloseCursor(cursor); } } while (true); return ret; }
4.1.2.2 资源不释放
动画泄漏
Bad Code
以下代码将创建一个持续运行的动画,但是没有停止或释放它,这可能会导致电池的损耗和性能问题:
public class AnimationLeakExample extends Activity { private ImageView imageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView = findViewById(R.id.image_view); startAnimation(); } private void startAnimation() { RotateAnimation animation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(1000); animation.setRepeatCount(Animation.INFINITE); imageView.startAnimation(animation); } }
Good Case
请注意,在实际使用中,应该在动画不再需要时停止或释放它,以避免泄漏和性能问题。例如:
public class AnimationLeakExample { private Animation animation; public AnimationLeakExample() { animation = new TranslateAnimation(0, 100, 0, 100); animation.setDuration(1000); animation.setRepeatCount(Animation.INFINITE); animation.start(); } public void stopAnimation() { animation.cancel(); } }
Solution
通过正确停止和释放动画,可以帮助确保设备的电池寿命和性能。
音频泄漏
String16 AudioFlinger::ThreadBase::getWakeLockTag() switch(mType){ case MIXER: return String16("AudioMix"); case DIRECT: return String16("AudioDirectOut"): case DUPLICATING: return String16("AudioDup"); case RECORD: return String16("AudioIn"); case 0FFLOAD: return String16("Audio0ffload"); case MMAP_PLAYBACK: return String16("MmAPPlayback"); case MMAP_CAPTURE: return String16("MmapCapture"): case SPATIALIZER: return String16("AudioSpatial"); default: ALOG_ASSERT(false); return String16("AudioUnknown"): }
Bad Code
以下代码将创建一个MediaPlayer并不会释放它,这可能会导致电池的损耗和性能问题:
public class AudioMix { private MediaPlayer mediaPlayer; public AudioLeakExample(Context context) { mediaPlayer = MediaPlayer.create(context, R.raw.sample_audio); mediaPlayer.start(); } }
Good Case
请注意,在实际使用中,应该在不再需要音频时释放MediaPlayer,以避免泄漏和性能问题。例如:
public class AudioMix { private MediaPlayer mediaPlayer; public AudioLeakExample(Context context) { mediaPlayer = MediaPlayer.create(context, R.raw.sample_audio); mediaPlayer.start(); } public void releaseMediaPlayer() { if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; } } }
Solution
正确释放MediaPlayer,可以帮助确保设备的电池寿命和性能。
WakeLock不释放
Bad Code
以下是一段示例代码,该代码演示了如何使用 WakeLock
来保持设备唤醒,并且不释放 WakeLock
导致电池损耗和性能问题。
public class MainActivity extends APPCompatActivity { private PowerManager.WakeLock mWakeLock; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakeLockTag"); mWakeLock.acquire(); } @Override protected void onDestroy() { super.onDestroy(); //注意这里没有释放WakeLock,导致电池损耗和性能问题 } }
Good Case
在这段代码中,在 onCreate
方法中获取了一个 WakeLock
,但是在 onDestroy
方法中没有释放该 WakeLock
。这样做会导致电池损耗和性能问题,因为设备将一直处于唤醒状态。
@Override protected void onDestroy() { super.onDestroy(); if (mWakeLock.isHeld()) { mWakeLock.release(); } }
Solution
为了解决这个问题,必须在不再需要该 WakeLock
时立即释放它,可以在 onDestroy
方法中添加以下代码实现,当该Activity不再需要保持设备唤醒时,就可以立即释放 WakeLock
,以避免电池损耗和性能问题。
4.1.2.3 高频调用耗时函数
高频调用md5校验耗时函数
Bad Code
这段代码是模拟高频率调用MD5校验函数导致电池损耗和性能问题的情形:
public class BatteryPerformanceProblem { public static void main(String[] args) { // 伪代码 while (true) { String data = "example data"; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data.getBytes()); byte[] hash = md.digest(); } catch (Exception e) { e.printStackTrace(); } } } }
这段代码会一直循环,不断地调用MD5校验函数,这将导致大量的CPU计算和内存使用,最终将导致电池损耗和性能问题。因此,在实际开发中,应该避免这样的高频率调用,控制对系统资源的使用。
Good Case
public class BatteryPerformanceProblem { public static void main(String[] args) { // 伪代码 if(临界条件){ String data = "example data"; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data.getBytes()); byte[] hash = md.digest(); } catch (Exception e) { e.printStackTrace(); } }else{ // Todo sth } } }
Solution
对高频调用md5校验耗时函数进行临界条件判断
高频调用网络建链耗时函数
Bad Code
这段代码是模拟高频率调用网络建连耗时函数导致电池损耗和性能问题的情形:
public class BatteryPerformanceProblem { public static void main(String[] args) { // 伪代码 while (true) { try { URL url = new URL("https://github.com/MicroKibaco"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); connection.disconnect(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } }
NetworkNotAvaliabeException: network not avaliable
Good Case
这段代码会一直循环,不断地调用网络建连函数,这将导致大量的网络数据传输和系统资源使用,最终将导致电池损耗和性能问题。因此,在实际开发中,应该避免这样的高频率调用,控制对系统资源的使用。
public class BatteryPerformanceProblem { public static void main(String[] args) { // 伪代码 if(临界条件){ try { URL url = new URL("https://github.com/MicroKibaco"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); connection.disconnect(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }else{ // Todo sth } } }
Solution
对高频调用网络建链耗时函数进行临界条件判断
高频调用正则表达式耗时函数
正则表达式是一种非常强大的工具,但是使用不当也可能导致性能问题。高频调用正则表达式的函数可能会占用大量的 CPU 资源,从而导致电池损耗和性能问题
Bad Case
这段代码中,当用户点击按钮时,将会使用正则表达式频繁地匹配字符串。如果该操作被频繁调用,可能会导致电池损耗和性能问题。
public class MainActivity extends APPCompatActivity { private Pattern mPattern = Pattern.compile("\d+"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final Button button = findViewById(R.id.button); final TextView textView = findViewById(R.id.textView); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String input = "The number is 123456."; Matcher matcher = mPattern.matcher(input); if (matcher.find()) { textView.setText(matcher.group()); } } }); } } 复制代码
Good Case
这段代码使用了 indexOf
方法代替正则表达式,以找到所需的数字。因为这种方法不需要使用正则表达式,因此它可以提高性能并减少电池损耗。
public class MainActivity extends APPCompatActivity { private Pattern mPattern = Pattern.compile("\d+"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final Button button = findViewById(R.id.button); final TextView textView = findViewById(R.id.textView); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String input = "The number is 123456."; Matcher matcher = mPattern.matcher(input); if (matcher.find()) { textView.setText(matcher.group()); } } }); } }
Solution
- 优化正则表达式:尽量简化正则表达式,避免使用不必要的重复匹配。
- 缓存结果:如果正在匹配相同的字符串,可以考虑缓存结果,以避免多次调用正则表达式。
- 使用其他技术:使用字符串函数代替正则表达式,以提高性能。
4.1.3 CPU异常结论
- 手机电池温度一般在37度以下,不会触发CPU温控限制
- 手机未充电且30%电量以上,CPU使用率比较稳定
- 手机省电模式对CPU影响较大
- 手机网络类型对CPU影响很小
4.1.4 CPU异常阈值
- 手机电池电量>=30,电池温度<=37度
- 不同CPU型号 * 不同场景
- CPU劣化组的P90使用率
4.2 Camera功耗
Camera功耗原因
Camera 功耗主要是由相机传感器,图像处理芯片,液晶显示器和自动对焦等组件造成的。
对于直播APP而言,最快捷的方式是限制相机的帧率,以减少图像处理所需的时间和电
因为高分辨率高帧率的录制会带来快速的功耗消耗和温升,像抖音的开播场景,Camera功耗 200mA+,占整机的25%以上
Camera功耗场景
抖音在开播请求的采集帧率是30fps,但只使用了15fps
Camera优化手段
那么可以主动下调采集帧率
30FPS 下调到15FPS后CPU下降13% ,整机功耗下降120mA
Camera camera = Camera.open(); Camera.Parameters params = camera.getParameters(); List<int[]> supportedFpsRanges = params.getSupportedPreviewFpsRange(); int[] targetRange = null; for (int[] fpsRange : supportedFpsRanges) { if (fpsRange[0] == 15000 && fpsRange[1] == 15000) { targetRange = fpsRange; break; } } if (targetRange != null) { params.setPreviewFpsRange(targetRange[0], targetRange[1]); } else { // 15 FPS is not supported, use a different value } camera.setParameters(params); camera.startPreview();
请注意,某些设备可能不支持 15 FPS,因此在代码中应该加入错误处理逻辑。此外,如果要使用相机,请务必注意遵循 Android 平台的隐私和权限规则。
4.3 低功耗
4.3.1 低功耗背景
低电量情况下,用户对功耗更加敏感,高功耗任务需要更激进的功耗优化措施
- 未充电的情况下,剩余电量<=5%,单次停留时长开始显著下降
- 剩余最低电量在2%-5%的,会话占比1.2%,时长在15-18分钟左右,对比 剩余10%的平均时长27分钟,要损失约11分钟
4.3.2 低功耗模式
Animation暂停
Android为了电池优化会在设备电量较低时自动减少某些可视化效果,例如动画、滚动和转场效果等。如果需要手动暂停动画,可以使用以下代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (getWindow() != null) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); } }
该代码会在Android 4.4及以上版本中启用硬件加速,并且会自动暂停所有正在运行的动画,以降低电量消耗。需要注意的是,这个方法可能会导致某些视图出现问题,因此需要根据具体情况决定是否使用。
超分算降级
超分辨率算法是将低分辨率图像恢复到高分辨率图像的过程。相反,降分辨率则是将高分辨率图像转换为低分辨率图像。下面是一个使用OpenCV库进行图像降采样的Python代码示例:
import cv2 # 读入高分辨率图像 image = cv2.imread("high_res_image.jpg") # 降采样图像 downsampled = cv2.pyrDown(image) # 保存降采样后的图像 cv2.imwrite("low_res_image.jpg", downsampled)
在上面的代码中,cv2.imread
函数读取高分辨率图像,cv2.pyrDown
函数对其进行降采样,最后, 使用cv2.imwrite
函数将降采样后的图像保存到磁盘上。需要注意的是,降采样会丢失图像的某些细节,因此应该谨慎使用。
定位服务降级
以下是一个简单的示例代码,用于在定位服务中使用省电模式:
// 获取定位管理器 LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); // 创建定位请求 LocationRequest locationRequest = LocationRequest.create() .setPriority(LocationRequest.PRIORITY_LOW_POWER) .setInterval(30000) // 30秒更新一次位置 .setFastestInterval(15000); // 最快15秒更新一次位置 // 检查定位服务是否可用 if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { // 请求位置更新 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, locationRequest, locationListener); } else { // 如果GPS不可用,则请求网络位置更新 locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, locationRequest, locationListener); }
在这个示例代码中,高功耗任务首先获取了定位管理器的实例,然后,创建了一个定位请求,该请求使用了较低的电量优先级。
然后,高功耗任务检查了GPS是否可用,如果可用,高功耗任务就使用GPS请求位置更新。否则,高功耗任务使用网络提供商请求位置更新。
在这种情况下,高功耗任务使用了一个较长的位置更新间隔(30秒)和较短的最快更新间隔(15秒),以便在省电模式下更好地优化电量消耗。
Senor降级
以下是一个简单的示例代码,用于在Sensor服务中使用低功耗模式:
// 获取SensorManager实例 SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); // 获取加速度传感器实例 Sensor accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); // 创建SensorEventListener SensorEventListener sensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // 处理传感器数据 } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // 不需要处理 } }; // 注册传感器监听器 if (accelerometerSensor != null) { // 使用SENSOR_DELAY_NORMAL模式,该模式下传感器数据更新速度较慢,可以节省电量 sensorManager.registerListener(sensorEventListener, accelerometerSensor, SensorManager.SENSOR_DELAY_NORMAL); }
在这个示例代码中,高功耗任务获取了SensorManager的实例,然后,获取了一个加速度传感器的实例。
接着,任务创建了一个SensorEventListener来监听传感器数据的变化。
最后,高功耗任务使用SensorManager的registerListener方法注册了这个传感器监听器,使用了较慢的SENSOR_DELAY_NORMAL模式,以便在低功耗模式下更好地优化电量消耗。
这样,高功耗任务就实现了Sensor降级的效果。需要注意的是,在不需要使用传感器的时候,应该及时取消注册传感器监听器,以免造成资源浪费。
视频码率降级
以下是一个简单的示例代码,用于在视频播放中降低码率以减少数据流量消耗:
// 获取MediaPlayer实例 MediaPlayer mediaPlayer = new MediaPlayer(); // 设置视频数据源 mediaPlayer.setDataSource(videoUrl); // 设置视频播放画质 mediaPlayer.setVideoQuality(MediaPlayer.VIDEOQUALITY_LOW); // 准备MediaPlayer mediaPlayer.prepare(); // 播放视频 mediaPlayer.start();
在这个示例代码中,高功耗任务首先获取了MediaPlayer的实例,然后,设置了视频的数据源,接着设置了视频的播放画质为低画质。
最后,高功耗任务调用prepare方法准备MediaPlayer,然后,调用start方法开始播放视频。
这样,就实现了在视频播放中降低码率的效果,以减少数据流量消耗。
需要注意的是,在选择降低码率的同时,也会降低视频的清晰度和流畅度,需要根据具体场景进行权衡。
Off-Screen Rendering
以下是一个简单的示例代码,用于关闭离屏渲染(Off-screen rendering):
// 获取当前View的硬件加速类型 int currentLayerType = view.getLayerType(); // 关闭硬件加速 view.setLayerType(View.LAYER_TYPE_NONE, null);
在这个示例代码中,高功耗任务获取了当前View的硬件加速类型,然后,使用setLayerType方法将硬件加速类型设置为LAYER_TYPE_NONE,即关闭硬件加速,从而关闭离屏渲染。
需要注意的是,关闭硬件加速可能会降低视图渲染的性能和效率,因此在实际APP中,应该根据具体需求和场景来决定是否关闭硬件加速。
MediaPlayer降级
以下是一个简单的示例代码,用于在视频播放器中实现降级:
// 获取MediaPlayer实例 MediaPlayer mediaPlayer = new MediaPlayer(); // 设置视频数据源 mediaPlayer.setDataSource(videoUrl); try { // 尝试使用硬件解码器 mediaPlayer.setHardwareDecoderEnabled(true); // 准备MediaPlayer mediaPlayer.prepare(); } catch (Exception e) { // 硬件解码器不可用,使用软件解码器 mediaPlayer.setHardwareDecoderEnabled(false); // 准备MediaPlayer mediaPlayer.prepare(); } // 播放视频 mediaPlayer.start();
在这个示例代码中,高功耗任务首先获取了MediaPlayer的实例。然后,设置了视频的数据源。
接着,高功耗任务尝试开启硬件解码器,如果硬件解码器不可用,就使用软件解码器。最后,高功耗任务调用prepare方法准备MediaPlayer。然后,调用start方法开始播放视频。
这样,就实现了在视频播放器中进行降级的效果。需要注意的是,软件解码器通常会比硬件解码器消耗更多的CPU和内存资源,因此在实际APP中,应该根据具体需求和场景来决定是否使用软件解码器。
4.3.3 生效的场景
非充电情况下电量低于30%
易发热机型增加开启时长
4.3.4 低功耗收益
降低xxxmA
4.4 热缓解
4.4.1 热缓解背景
手机发热会影响用户体验,并且系统会通过限制设备的使用,减少发热量。前台APP 不及时进行调整的话很可能会出现卡顿。
4.4.2 热缓解方案
监控手机发热,在发热前主动进行激进的降级手段,减少资源消耗,减少发热以及卡顿的发生
以上图片来源于字节技术沙龙
4.4.3 热状态代码
Google官方在N版本以上提供了热缓解框架,热缓解框架热状态码详见下表:
虽然Google热缓解框架需要各OEM厂商适配,但OEM并没有适配热缓解框架,而是通过开放SDK里提供了类似的能力,支持度和灵敏度,兼容性更优于Google热缓解框架
4.4.4 收拢厂商SDK兼容热缓解
因为没法磨平厂商的差异性,因此高功耗任务需要对各厂商的SDK进行适配,提供统一的温控级别,并对各个厂商的SDK进行收拢,下图可以看到字节对华为、小米、Vivo和OPPO的厂商热缓解框架SDK收拢架构图
以上图片来源于字节技术沙龙
4.4.4.1 厂商的热缓解SDK优点
提供壳温,高热识别更灵敏
4.4.4.2 厂商的热缓解SDK缺点
老机型不支持,需要升级版本,覆盖率不高
4.4.5 电池温度VS壳温相关性分析
系统在壳温平均43度左右会开启温控限制,39度左右解除限制
壳温与电池温度相关,壳温高于电池温度约2度
4.5 动态帧率
4.5.1 背景
绘制帧率可以很明显的影响GPU功耗。抖音大部分的视频帧率都是30fps,叠加上各种动画(音乐转盘,跑马灯,进度条,活动动画),绘制帧率会处于60fps,降低绘制帧率可以有效优化GPU耗电。
以上图片来源于字节技术沙龙
4.5.2 优化方向
4.5.2.1 帧率优化
动画帧率控制
动画帧率对齐
动画与播放器帧率对齐
4.5.2.3 厂商合作
调节vsync回调频率,实现动态帧率
4.5.2.4 Kita框架
Kita框架通过托管多种动画的绘制流程,将整体的动 画调度逻辑在kita controller中进行统一管理,通过降帧,对齐后,实现了整体绘制帧率的降低
以上图片来源于字节技术沙龙
厂商通过调节Vsync的回调频率或者利用LTPO屏幕的动态刷新率能力,都可以从系统侧实现对APP绘制帧率的控制,高功耗任务和厂商合作
以上图片来源于字节技术沙龙
在视频推荐页通知系统降低帧率到30fps,在部分场景恢复正常帧率(如弹幕,高帧率视频场景),可以在保证用户体验的情况下获取更大的功耗收益。
4.6 SurfaceView替换TextureView
4.6.1 背景
TextureView和SurfaceView是两个最常用的播放视频控件。
以上图片来源于字节技术沙龙
TextureView控件位 于主图层上,解码器将视频帧传递到TextureView对象还需要GPU做一次绘制才能在屏幕上显示,所以其功耗更高,消耗内存更大,CPU占用率也更高。
以上图片来源于字节技术沙龙
以下是TextureView和SurfaceView对比图:
以上图片来源于字节技术沙龙
4.6.2 收益
CPU -xxx%
整机功耗 -xxxmA
五、功耗APM监控建设
技术需求: 将前台的WakeLock、Location、Alarm、CPU、Net和Sensor等器件的消费时间和启动次数,以及前台总耗电、前台单位时长总耗电、前台模块单位时间耗电、前台总时长进行计算并将结果存储到SD卡上的JSON配置文件。后台读取JSON配置文件的消费时间、启动次数、前台总耗电、前台单位时长总耗电、前台模块单位时间耗电、前台总时长等字段后并进行上报,监控系统可以进行多样化消费阈值的定义。如果超过消费阈值,那么飞书机器人告警。
简单列举一下WakeLock监控,其他器件参考WakeLock监控完成。实现思路如下:
- 下载ASM框架,并将其添加到Android项目的依赖中。ASM是一个Java字节码操作库,可以用来修改现有的Java字节码,从而实现对类和方法的插桩。
- 编写一个ASM插件,该插件将在应用程序启动时加载,并通过字节码插桩来修改应用程序代码以实现监控WakeLock。
- 在插件中使用ASM插桩技术,查找应用程序中所有使用WakeLock的地方,并插入代码以记录WakeLock的启动时间和使用时间。这些信息将保存在内存中,并在WakeLock释放时写入SD卡上的JSON配置文件中。
- 为了在应用程序中读取JSON配置文件中的信息,需要创建一个后台进程,该进程可以定期读取JSON配置文件,并将信息存储在数据库或发送给服务器。
public class WakeLockPlugin implements ClassVisitor { public WakeLockPlugin(ClassVisitor cv) { super(Opcodes.ASM6, cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("acquire") && desc.equals("(J)V")) { return new WakeLockAcquireMethodVisitor(mv); } else if (name.equals("release") && desc.equals("()V")) { return new WakeLockReleaseMethodVisitor(mv); } return mv; } private class WakeLockAcquireMethodVisitor extends MethodVisitor { public WakeLockAcquireMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM6, mv); } @Override public void visitCode() { super.visitCode(); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/os/SystemClock", "elapsedRealtime", "()J", false); mv.visitVarInsn(Opcodes.LSTORE, 1); } } private class WakeLockReleaseMethodVisitor extends MethodVisitor { public WakeLockReleaseMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM6, mv); } @Override public void visitCode() { super.visitCode(); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/os/SystemClock", "elapsedRealtime", "()J", false); mv.visitVarInsn(Opcodes.LLOAD, 1); mv.visitInsn(Opcodes.LSUB); mv.visitVarInsn(Opcodes.LSTORE, 2); mv.visitLdcInsn("wakelock.json"); mv.visitLdcInsn("time"); mv.visitVarInsn(Opcodes.LLOAD, 2); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/json/JSONObject", "put", "(Ljava/lang/String;J)Lorg/json/JSONObject;", false); mv.visitLdcInsn("wakelock.json"); mv.visitLdcInsn("count"); mv.visitInsn(Opcodes.ICONST_1); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/json/JSONObject", "put", "(Ljava/lang/String;I)Lorg/json/JSONObject;", false); mv.visitLdcInsn("wakelock.json"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/os/Environment", "getExternalStorageDirectory", "()Ljava/io/File;", false); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/File", "getAbsolutePath", "()Ljava/lang/String;", false); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/myapplication/JsonUtils", "writeJsonFile", "(Ljava/lang/String;Lorg/json/JSONObject;Ljava/lang/String;)V", false); } } }
在上面的代码中,我们创建了一个ClassVisitor的子类,该子类用于访问类中的方法,并在必要时修改字节码。在visitMethod方法中,我们检查每个方法的名称和描述符,如果它们匹配acquire和release方法的名称和描述符,就创建相应的MethodVisitor。
在WakeLockAcquireMethodVisitor中,我们在方法的开头插入代码,以获取当前时间,并将其保存在局部变量中。这个时间将在释放WakeLock时使用。
在WakeLockReleaseMethodVisitor中,我们在方法的开头插入代码,以获取当前时间,并计算WakeLock的使用时间。然后,我们将时间和计数器保存到JSON对象中,然后将JSON对象写入SD卡上的文件中。
最后,在主程序中,我们需要在应用程序启动时加载WakeLockPlugin,并且每隔一段时间读取SD卡上的JSON配置文件,并将其写入数据库或发送到服务器。
public class MainActivity extends AppCompatActivity { @Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Load WakeLockPlugintry { ClassReader cr = new ClassReader(WakeLock.class.getName()); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); WakeLockPlugin wp = new WakeLockPlugin(cw); cr.accept(wp, ClassReader.EXPAND_FRAMES); byte[] code = cw.toByteArray(); DexFile df = new DexFile(getPackageCodePath()); df.writeDexFile("classes.dex", code); } catch (IOException e) { e.printStackTrace(); } // Start background service to read JSON configuration fileIntent intent = new Intent(this, ConfigService.class); startService(intent); } }
在上面的代码中,我们在应用程序启动时加载WakeLockPlugin,并将其转换为Dex格式的字节码。然后,我们启动一个后台服务ConfigService,该服务将定期读取SD卡上的JSON配置文件,并将其写入数据库或发送到服务器。
以上就是在Android应用程序中使用ASM完成字节码插桩,实现WakeLock的监控并将其保存到SD卡上的JSON配置文件中的基本步骤。请注意,这只是一个示例,实际应用程序可能需要根据具体需求进行更改和调整。
六、总结&展望
Android功耗优化指导规范就讲解完毕了,简单的总结一下: Android功耗优化指导规范主要分为五部分内容,第一部分内容是5W2H分析功耗优化。第二部分内容是功耗优化技术方案。第三部分内容是功耗优化方案分析。第四部分内容是功耗APM监控建设。最后一部分内容是总结与展望。
针对常见功耗问题,目前主要从两个视角发现问题,第一个视角是从设备角度出发寻找功耗优化手段,第二个视角是从体验角度出发,衡量如何使用降级手段,如低功耗模式和热缓解等。
针对常见功耗问题,未来我们不但要建立完善不同器件的功耗异常检测框架,同时也要完善功耗防劣化能力。
我是小木箱,如果大家对我的文章感兴趣,那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营,一个专注移动端分享的互联网成长社区。
参考资料
- 抖音功耗优化实践
- 一种AndroidAPP耗电定位方案
- 大众点评APP的短视频耗电量优化实战
- 电量优化 - 电量的统计原理与监控
- 优化电池位置
- 定位服务
- Android Vitals
- Android耗电原理及飞书耗电治理
- 第十二期字节跳动技术沙龙直播回放(02:09:00-02:45:00)
- 第十二期字节跳动技术沙龙直播回放PPT下载地址(密码: 0d9s)
- 奔跑吧!智能Monkey之Fastbot跨平台