功耗优化 · 方案篇 · 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监控建设。最后一部分内容是总结与展望。

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

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

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

参考资料


相关文章
|
1月前
|
Java Android开发
Android面试题经典之Glide取消加载以及线程池优化
Glide通过生命周期管理在`onStop`时暂停请求,`onDestroy`时取消请求,减少资源浪费。在`EngineJob`和`DecodeJob`中使用`cancel`方法标记任务并中断数据获取。当网络请求被取消时,`HttpUrlFetcher`的`cancel`方法设置标志,之后的数据获取会返回`null`,中断加载流程。Glide还使用定制的线程池,如AnimationExecutor、diskCacheExecutor、sourceExecutor和newUnlimitedSourceExecutor,其中某些禁止网络访问,并根据CPU核心数动态调整线程数。
77 2
|
8天前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
28 4
|
13天前
|
缓存 算法 数据库
安卓应用性能优化:一场颠覆平凡的极限挑战,拯救卡顿的惊世之战!
【8月更文挑战第7天】《安卓应用性能优化实战》
26 4
|
3天前
|
编译器 Android开发 开发者
Android经典实战之Kotlin 2.0 迁移指南:全方位优化与新特性解析
本文首发于公众号“AntDream”。Kotlin 2.0 已经到来,带来了 K2 编译器、多平台项目支持、智能转换等重大改进。本文提供全面迁移指南,涵盖编译器升级、多平台配置、Jetpack Compose 整合、性能优化等多个方面,帮助开发者顺利过渡到 Kotlin 2.0,开启高效开发新时代。
6 0
|
6天前
|
监控 开发工具 Android开发
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
本文介绍了GB28181标准中的状态信息报送机制,即心跳机制,用于监控设备与服务器间的连接状态。根据国标GB/T28181-2016,设备在异常时需立即发送状态信息,在正常状态下则按固定间隔(默认60秒)定期发送。若连续三次(默认值)未收到心跳,则视为离线。文章展示了在Android平台的GB28181设备接入模块(SmartGBD)中,如何调整心跳间隔为20秒及超时次数为3次,并给出了心跳消息的示例和异常处理代码片段。对于希望深入了解或遇到问题的开发者,作者提供了进一步交流的机会。
|
6天前
|
Web App开发 网络协议 Android开发
### 惊天对决!Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【8月更文挑战第14天】随着移动互联网的发展,实时音视频通信已成为移动应用的关键部分。本文对比分析了Android平台上WebRTC、RTMP与RTSP三种主流技术方案。WebRTC提供端到端加密与直接数据传输,适于高质量低延迟通信;RTMP适用于直播场景,但需服务器中转;RTSP支持实时流播放,但在复杂网络下稳定性不及WebRTC。三种方案各有优劣,WebRTC功能强大但集成复杂,RTMP和RTSP实现较简单但需额外编码支持。本文还提供了示例代码以帮助开发者更好地理解和应用这些技术。
23 0
|
1月前
|
算法 Java API
Android性能优化面试题经典之ANR的分析和优化
Android ANR发生于应用无法在限定时间内响应用户输入或完成操作。主要条件包括:输入超时(5秒)、广播超时(前台10秒/后台60秒)、服务超时及ContentProvider超时。常见原因有网络、数据库、文件操作、计算任务、UI渲染、锁等待、ContentProvider和BroadcastReceiver的不当使用。分析ANR可借助logcat和traces.txt。主线程执行生命周期回调、Service、BroadcastReceiver等,避免主线程耗时操作
37 3
|
2月前
|
缓存 JSON 网络协议
Android面试题:App性能优化之电量优化和网络优化
这篇文章讨论了Android应用的电量和网络优化。电量优化涉及Doze和Standby模式,其中应用可能需要通过用户白名单或电池广播来适应限制。Battery Historian和Android Studio的Energy Profile是电量分析工具。建议减少不必要的操作,延迟非关键任务,合并网络请求。网络优化包括HTTPDNS减少DNS解析延迟,Keep-Alive复用连接,HTTP/2实现多路复用,以及使用protobuf和gzip压缩数据。其他策略如使用WebP图像格式,按网络质量提供不同分辨率的图片,以及启用HTTP缓存也是有效手段。
56 9
|
2月前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
46 3
|
1月前
|
安全 Java 数据处理
Android多线程编程实践与优化技巧
Android多线程编程实践与优化技巧