功耗优化 · 方案篇 · Android功耗优化指导规范(2)

简介: 功耗优化 · 方案篇 · Android功耗优化指导规范

四、功耗优化案例分析

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使用率。可以根据具体需求,来修改代码中的处理逻辑。

image.png

在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

对高频调用网络建链耗时函数进行临界条件判断

image.png

高频调用正则表达式耗时函数

正则表达式是一种非常强大的工具,但是使用不当也可能导致性能问题。高频调用正则表达式的函数可能会占用大量的 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
  1. 优化正则表达式:尽量简化正则表达式,避免使用不必要的重复匹配。
  2. 缓存结果:如果正在匹配相同的字符串,可以考虑缓存结果,以避免多次调用正则表达式。
  3. 使用其他技术:使用字符串函数代替正则表达式,以提高性能。

4.1.3 CPU异常结论

  • 手机电池温度一般在37度以下,不会触发CPU温控限制
  • 手机未充电且30%电量以上,CPU使用率比较稳定
  • 手机省电模式对CPU影响较大
  • 手机网络类型对CPU影响很小

4.1.4 CPU异常阈值

  • 手机电池电量>=30,电池温度<=37度
  • 不同CPU型号 * 不同场景
  • CPU劣化组的P90使用率

4.2 Camera功耗

Camera功耗原因

Camera 功耗主要是由相机传感器,图像处理芯片,液晶显示器和自动对焦等组件造成的。

image.png

对于直播APP而言,最快捷的方式是限制相机的帧率,以减少图像处理所需的时间和电

因为高分辨率高帧率的录制会带来快速的功耗消耗和温升,像抖音的开播场景,Camera功耗 200mA+,占整机的25%以上

image.png

Camera功耗场景

抖音在开播请求的采集帧率是30fps,但只使用了15fps

Camera优化手段

那么可以主动下调采集帧率

image.png

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 平台的隐私和权限规则。

image.png

4.3 低功耗

4.3.1 低功耗背景

低电量情况下,用户对功耗更加敏感,高功耗任务需要更激进的功耗优化措施

  • 未充电的情况下,剩余电量<=5%,单次停留时长开始显著下降
  • 剩余最低电量在2%-5%的,会话占比1.2%,时长在15-18分钟左右,对比 剩余10%的平均时长27分钟,要损失约11分钟

4.3.2 低功耗模式

image.png

Animation暂停

image.png

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及以上版本中启用硬件加速,并且会自动暂停所有正在运行的动画,以降低电量消耗。需要注意的是,这个方法可能会导致某些视图出现问题,因此需要根据具体情况决定是否使用。

超分算降级

image.png

超分辨率算法是将低分辨率图像恢复到高分辨率图像的过程。相反,降分辨率则是将高分辨率图像转换为低分辨率图像。下面是一个使用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函数将降采样后的图像保存到磁盘上。需要注意的是,降采样会丢失图像的某些细节,因此应该谨慎使用。

定位服务降级

image.png

以下是一个简单的示例代码,用于在定位服务中使用省电模式:

// 获取定位管理器
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降级

image.png

以下是一个简单的示例代码,用于在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降级的效果。需要注意的是,在不需要使用传感器的时候,应该及时取消注册传感器监听器,以免造成资源浪费。

视频码率降级

image.png

以下是一个简单的示例代码,用于在视频播放中降低码率以减少数据流量消耗:

// 获取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

image.png

以下是一个简单的示例代码,用于关闭离屏渲染(Off-screen rendering):

// 获取当前View的硬件加速类型
int currentLayerType = view.getLayerType();
// 关闭硬件加速
view.setLayerType(View.LAYER_TYPE_NONE, null);

在这个示例代码中,高功耗任务获取了当前View的硬件加速类型,然后,使用setLayerType方法将硬件加速类型设置为LAYER_TYPE_NONE,即关闭硬件加速,从而关闭离屏渲染。

需要注意的是,关闭硬件加速可能会降低视图渲染的性能和效率,因此在实际APP中,应该根据具体需求和场景来决定是否关闭硬件加速。

MediaPlayer降级

image.png

以下是一个简单的示例代码,用于在视频播放器中实现降级:

// 获取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 热缓解方案

监控手机发热,在发热前主动进行激进的降级手段,减少资源消耗,减少发热以及卡顿的发生

image.png

以上图片来源于字节技术沙龙

4.4.3 热状态代码

Google官方在N版本以上提供了热缓解框架,热缓解框架热状态码详见下表:

image.png

虽然Google热缓解框架需要各OEM厂商适配,但OEM并没有适配热缓解框架,而是通过开放SDK里提供了类似的能力,支持度和灵敏度,兼容性更优于Google热缓解框架

image.png

4.4.4 收拢厂商SDK兼容热缓解

因为没法磨平厂商的差异性,因此高功耗任务需要对各厂商的SDK进行适配,提供统一的温控级别,并对各个厂商的SDK进行收拢,下图可以看到字节对华为、小米、Vivo和OPPO的厂商热缓解框架SDK收拢架构图

image.png

以上图片来源于字节技术沙龙

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耗电。

image.png

以上图片来源于字节技术沙龙

4.5.2 优化方向

4.5.2.1 帧率优化

动画帧率控制

动画帧率对齐

动画与播放器帧率对齐

4.5.2.3 厂商合作

调节vsync回调频率,实现动态帧率

4.5.2.4 Kita框架

Kita框架通过托管多种动画的绘制流程,将整体的动 画调度逻辑在kita controller中进行统一管理,通过降帧,对齐后,实现了整体绘制帧率的降低

image.png

以上图片来源于字节技术沙龙

厂商通过调节Vsync的回调频率或者利用LTPO屏幕的动态刷新率能力,都可以从系统侧实现对APP绘制帧率的控制,高功耗任务和厂商合作

image.png

以上图片来源于字节技术沙龙

在视频推荐页通知系统降低帧率到30fps,在部分场景恢复正常帧率(如弹幕,高帧率视频场景),可以在保证用户体验的情况下获取更大的功耗收益。

4.6 SurfaceView替换TextureView

4.6.1 背景

TextureView和SurfaceView是两个最常用的播放视频控件。

image.png

以上图片来源于字节技术沙龙

TextureView控件位 于主图层上,解码器将视频帧传递到TextureView对象还需要GPU做一次绘制才能在屏幕上显示,所以其功耗更高,消耗内存更大,CPU占用率也更高。

image.png

以上图片来源于字节技术沙龙

以下是TextureView和SurfaceView对比图:

image.png

以上图片来源于字节技术沙龙

4.6.2 收益

CPU -xxx%

整机功耗 -xxxmA

五、功耗APM监控建设

image.png

技术需求: 将前台的WakeLock、Location、Alarm、CPU、Net和Sensor等器件的消费时间和启动次数,以及前台总耗电、前台单位时长总耗电、前台模块单位时间耗电、前台总时长进行计算并将结果存储到SD卡上的JSON配置文件。后台读取JSON配置文件的消费时间、启动次数、前台总耗电、前台单位时长总耗电、前台模块单位时间耗电、前台总时长等字段后并进行上报,监控系统可以进行多样化消费阈值的定义。如果超过消费阈值,那么飞书机器人告警。

image.png

简单列举一下WakeLock监控,其他器件参考WakeLock监控完成。实现思路如下:

  1. 下载ASM框架,并将其添加到Android项目的依赖中。ASM是一个Java字节码操作库,可以用来修改现有的Java字节码,从而实现对类和方法的插桩。
  2. 编写一个ASM插件,该插件将在应用程序启动时加载,并通过字节码插桩来修改应用程序代码以实现监控WakeLock。
  3. 在插件中使用ASM插桩技术,查找应用程序中所有使用WakeLock的地方,并插入代码以记录WakeLock的启动时间和使用时间。这些信息将保存在内存中,并在WakeLock释放时写入SD卡上的JSON配置文件中。
  4. 为了在应用程序中读取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监控建设。最后一部分内容是总结与展望。

针对常见功耗问题,目前主要从两个视角发现问题,第一个视角是从设备角度出发寻找功耗优化手段,第二个视角是从体验角度出发,衡量如何使用降级手段,如低功耗模式和热缓解等。

针对常见功耗问题,未来我们不但要建立完善不同器件的功耗异常检测框架,同时也要完善功耗防劣化能力。

我是小木箱,如果大家对我的文章感兴趣,那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营,一个专注移动端分享的互联网成长社区。

参考资料


相关文章
|
3月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
68 20
Android经典面试题之图片Bitmap怎么做优化
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
55 4
|
27天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
3月前
|
开发框架 Dart 前端开发
Android 跨平台方案对比之Flutter 和 React Native
本文对比了 Flutter 和 React Native 这两个跨平台移动应用开发框架。Flutter 使用 Dart 语言,提供接近原生的性能和丰富的组件库;React Native 则基于 JavaScript,具备庞大的社区支持和灵活性。两者各有优势,选择时需考虑团队技能和项目需求。
390 8
|
3月前
|
Java Android开发 UED
安卓应用开发中的内存管理优化技巧
在安卓开发的广阔天地里,内存管理是一块让开发者既爱又恨的领域。它如同一位严苛的考官,时刻考验着开发者的智慧与耐心。然而,只要我们掌握了正确的优化技巧,就能够驯服这位考官,让我们的应用在性能和用户体验上更上一层楼。本文将带你走进内存管理的迷宫,用通俗易懂的语言解读那些看似复杂的优化策略,让你的开发之路更加顺畅。
64 2
|
3月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
3月前
|
Web App开发 网络协议 Android开发
Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【9月更文挑战第4天】本文详细对比了在Android平台上实现一对一音视频通话时常用的WebRTC、RTMP及RTSP三种技术方案。从技术原理、性能表现与开发难度等方面进行了深入分析,并提供了示例代码。WebRTC适合追求低延迟和高质量的场景,但开发成本较高;RTMP和RTSP则在简化开发流程的同时仍能保持较好的传输效果,适用于不同需求的应用场景。
174 1
|
3月前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
76 0
|
3月前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
154 0
|
4月前
|
存储 缓存 前端开发
安卓开发中的自定义控件实现及优化策略
【8月更文挑战第31天】在安卓应用的界面设计中,自定义控件是提升用户体验和实现特定功能的关键。本文将引导你理解自定义控件的核心概念,并逐步展示如何创建一个简单的自定义控件,同时分享一些性能优化的技巧。无论你是初学者还是有一定经验的开发者,这篇文章都会让你对自定义控件有更深的认识和应用。