技术经验谈 技术经验谈
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

hss01248

一号线程序员
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 最佳实践

    • Android数据库操作最佳实践
    • 基于蒲公英平台的app发布,更新,反馈功能的实现
    • testRss
    • Android保存图片或视频到相册的最佳实践
    • Android上图片视频选择功能的最佳实践
    • 一行代码完成startActivityForResult
    • android app后台运行的实现
      • ContentObserver
      • Foreground Service
      • 引导到电池优化设置界面:
  • ui

  • 优化

  • aop

  • apm

  • 架构

  • webview

  • rxjava

  • activity-fragment-view的回调和日志
  • Android加密相关
  • Android命令行操作
  • app后台任务
  • kotlin
  • kotlin漫谈
  • kotlin语言导论
  • sentry上传mapping.txt文件
  • so放于远程动态加载方案
  • states
  • Xposed模块开发
  • 一个关于manifest合并的猥琐操作
  • 玩坏android存储
  • 获取本app的安装来源信息
  • Android
  • 最佳实践
hss01248
2025-12-29
目录

android app后台运行的实现

# Android app后台运行的实现

# 后台运行的几个实现方式

Xnip2025-12-29_14-34-41

# 后台运行的挑战:系统限制

在开发后台功能时,必须考虑 Android 的功耗管理机制:

  • Doze Mode (打盹模式):当设备不充电且屏幕关闭一段时间后,系统会限制应用访问网络和 CPU。
  • App Standby (应用待机):系统根据用户使用频率将应用分类,限制不常用应用的后台频率。
  • 厂商限制 (OEM Restrict):许多国产手机(如华为、小米、OPPO)有非常严格的电池管理策略,可能会无视官方 API 直接杀死后台进程。
    • 对策:通常需要引导用户在设置中将应用加入“电池优化白名单”或开启“自启动”。

# 我的需求:

边拍照边自动压缩,即使app退到后台,也能自动压缩

# 实现思路:

通过ContentObserver接收拍照/截屏等操作拿到的媒体文件变化事件,然后进行压缩,且app退到后台后,需要依然能接收到变化事件,依然能在子线程里进行压缩.

  1. app常驻后台,尽量不被系统杀死--> 需要使用Foreground Service
  2. 进程被杀掉就算了,不需要恢复后继续运行-->因为进程被杀时,对文件变化的监听也没有了,就不需要什么后续的压缩了
  3. 退到后台时,ContentObserver没有事件触发,需要将应用加到“电池优化白名单”

# ContentObserver

public class MediaContentObserver extends ContentObserver {
    private static final String TAG = "MediaContentObserver";
    private final Context context;

    public MediaContentObserver(Handler handler, Context context) {
        super(handler);
        this.context = context;
    }

    @Override
    public void onChange(boolean selfChange) {
        this.onChange(selfChange, null);
    }

    /**
     * 逻辑: 收到 onChange 后,立即尝试查询这个 URI。
     *
     * 查询成功(Cursor 有数据): 说明是 修改 (Update) 或 新增 (Insert) (如果之前不知道这个ID)。
     *
     * 查询失败(Cursor 为空或 null): 说明该数据刚被 删除 (Delete)。
     * @param selfChange True if this is a self-change notification.
     * @param uri The Uri of the changed content.
     */
    @Override
    public void onChange(boolean selfChange, Uri uri) {
        // 当图片变化发生时(如拍照、下载图片等),这个方法会被调用
        Log.d(TAG, "Content changed. Triggering compression work.:"+ uri.toString());

    }

    // ContentObserver
    //现状: Android 系统自带的 Provider(如 MediaStore, Contacts)以及大多数第三方 App 很少 严格遵守并发送这些 flag
    //只有回到前台时,才会收到这个监听,app在后台收不到这个监听
    //一张图片会被通知多次,新增: 先一个5,然后两个9. 删除:17
    //需要将后台模式/省电策略改成无限制,才能在后台监听图片变化收到变化通知

    Set<String> compressionSet = new ConcurrentSkipListSet<>();
    @Override
    public void onChange(boolean selfChange, Uri uri, int flags) {
        // flags 参数可能包含 ContentResolver.NOTIFY_INSERT, NOTIFY_UPDATE, NOTIFY_DELETE
        Log.d(TAG, "Content changed. Triggering compression work.:"+ uri+",flags:"+flags+",selfChange:"+selfChange);
        Map<String, Object> infos = ContentUriUtil.getInfos(uri);
        LogUtils.dTag(TAG, infos);
        if(flags ==5 || flags == 9){
            if("1".equals(infos.get(MediaStore.Images.Media.IS_PENDING)+"")){
                LogUtils.i("图片IS_PENDING,暂不压缩",uri);
                return;
            }
            //新增或修改
            String path = infos.get(MediaStore.Images.Media.DATA)+"";
            if(TextUtils.isEmpty(path) || "null".equals(path)){
                //relative_path=Download/终极图片压缩/normal/
                path = Environment.getExternalStorageDirectory().getAbsolutePath()
                        +"/"+infos.get(MediaStore.Images.Media.RELATIVE_PATH)
                        +infos.get(MediaStore.Images.Media.DISPLAY_NAME);
            }
            if(compressionSet.contains(path)){
                return;
            }
            String finalPath = path;
            compressionSet.add(path);
            ThreadUtils.executeByIoWithDelay(new ThreadUtils.SimpleTask<File>() {
                @Override
                public File doInBackground() throws Throwable {
                    //通过文件大小变化,exif变化来判断图片ai后处理是否已经最终完成
                    ImageReadyChecker.waitUntilImageReady(Utils.getApp(),uri);
                    return PhotoCompressHelper.compressOneFile(new File(finalPath),false);
                }

                @Override
                public void onSuccess(File result) {
                    compressionSet.remove(finalPath);
                    LogUtils.d("压缩成功",result);
                    //ToastUtils.showShort("压缩成功:"+infos.get(MediaStore.Images.Media.DISPLAY_NAME));
                }

                @Override
                public void onFail(Throwable t) {
                    super.onFail(t);
                    compressionSet.remove(finalPath);
                }
                //延时,避免有些图片正在ai后处理,导致文件损坏.
            },0, TimeUnit.SECONDS);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

添加监听:

    private void registerContentObserver() {
        if (handlerThread == null) {
            handlerThread = new HandlerThread("ContentObserverThread");
            handlerThread.start();
        }
        Handler handler = new Handler(handlerThread.getLooper());

        // 引入你之前实现的 ContentObserver 类
        mediaContentObserver = new MediaContentObserver(handler, getApplicationContext());

        getContentResolver().registerContentObserver(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                true,
                mediaContentObserver
        );
    }

    private void unregisterContentObserver() {
        if (mediaContentObserver != null) {
            getContentResolver().unregisterContentObserver(mediaContentObserver);
            mediaContentObserver = null;
        }
        if (handlerThread != null) {
            handlerThread.quitSafely();
            handlerThread = null;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Foreground Service

主要是要做版本兼容

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.MediaStore;

import androidx.core.app.ServiceCompat;

import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.blankj.utilcode.util.Utils;
import com.hss01248.finalcompress.R;
// 引入你的 WorkManager 相关的类

public class ImageObserverService extends Service {

    private ContentObserver mediaContentObserver;
    private HandlerThread handlerThread;
    private static final int NOTIFICATION_ID = 101;
    private static final String CHANNEL_ID = "ImageObserverChannel";
    // 确保这些常量在你 Service 类中定义
    private static final String CHANNEL_NAME = "图片压缩监控服务";
    @Override
    public void onCreate() {
        super.onCreate();
        // 1. 创建通知渠道(Android 8.0+ 必需)
        createNotificationChannel();
    }
    /**
     * 创建通知渠道。
     * Android O (API 26) 及以上版本需要此方法来显示通知。
     */
    private void createNotificationChannel() {
        // 只有在 API 级别大于等于 26 时才需要创建通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            // 1. 设置渠道属性
            // IMPORTANCE_DEFAULT 或 IMPORTANCE_LOW 通常用于前台服务,
            // 避免过于打扰用户。
            int importance = NotificationManager.IMPORTANCE_LOW;

            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    CHANNEL_NAME,
                    importance
            );

            // 可选:设置渠道的描述
            channel.setDescription("用于后台监测设备中新增图片并自动进行压缩处理。");

            // 2. 获取 NotificationManager
            NotificationManager notificationManager =
                    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

            // 3. 注册渠道
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
            }
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        LogUtils.d( "onStartCommand",intent,flags,startId);
        // 2. 启动前台服务 (核心步骤)
        startForegroundService();

        // 3. 注册 ContentObserver
        registerContentObserver();

        //askAwakeLock();//唤醒锁没有必要

        // 返回 START_STICKY 表示如果服务被系统杀死,系统会尝试重建服务
        return START_STICKY;
    }
    PowerManager.WakeLock wakeLock;
    private void askAwakeLock() {
        // 在 Service 的 onCreate 或 onStartCommand 中
        PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
// PARTIAL_WAKE_LOCK: 保持CPU运行,但允许屏幕关闭
         wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp:MyWakelockTag");

// 必须 acquire 才能生效
        wakeLock.acquire(30*60*1000L);
// 或者设定超时时间,防止忘记释放导致耗电过多
// wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/);

// 务必在 Service 销毁时释放!
// wakeLock.release();
    }

    // ---------------------- 核心方法 ----------------------

    private void startForegroundService() {
        Notification notification = createNotification();

        // --- 核心修改部分:使用版本判断实现 startForeground 兼容 ---

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10 (API 29) 及以上:
            // 需要使用带 serviceType 参数的 startForeground 方法

            // 确保你已经获取了正确的 serviceType
            int type = getForegroundServiceType2();

            // 调用 Android 10+ 版本的 startForeground(int id, Notification notification, int type)
            // IDE 可能会提示该方法需要 @RequiresApi,但运行时是正确的
            startForeground(NOTIFICATION_ID, notification, type);

        } else {
            // Android 9 (API 28) 及以下:
            // 使用传统的 startForeground(int id, Notification notification) 方法
            startForeground(NOTIFICATION_ID, notification);
        }
    }

    private int getForegroundServiceType2() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
        }
        return 0; // 旧版本无需指定类型
    }

    private Notification createNotification() {
        // 构建通知,必须给用户一个可见的提醒
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            return new Notification.Builder(this, CHANNEL_ID)
                    .setContentTitle("图片后台监控服务")
                    .setContentText("正在后台监测新图片并自动压缩...")
                    .setSmallIcon(R.drawable.ic_launcher) // 替换为你的图标
                    // 可选:添加点击通知时的 PendingIntent
                    // .setContentIntent(pendingIntent)
                    .setTicker("图片监测已启动")
                    .build();
        }
        //todo
        return  null;
    }



    // ---------------------- 生命周期管理 ----------------------

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 4. 服务停止时,注销 ContentObserver
        unregisterContentObserver();
        // 如果服务正在前台运行,将其移除
        ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH);
        if(wakeLock != null){
            wakeLock.release();
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        // 对于未绑定服务,返回 null
        return null;
    }

    // ... 其他方法 ...


    public static void startService() {
        //要确保在前台启动服务
        ThreadUtils.getMainHandler().postDelayed(new Runnable() {
            @Override
            public void run() {
                // 在 Activity 或 BroadcastReceiver 中
                Intent serviceIntent = new Intent(Utils.getApp(), ImageObserverService.class);

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    // 使用 startForegroundService()
                    Utils.getApp().startForegroundService(serviceIntent);
                } else {
                    // 对于旧版本,直接使用 startService()
                    Utils.getApp().startService(serviceIntent);
                }
                BatteryUtils.requestIgnoreBatteryOptimizations();
            }
        },3000);

    }

    public static void stopService() {
        // 在 Activity 或 BroadcastReceiver 中
        Intent serviceIntent = new Intent(Utils.getApp(), ImageObserverService.class);
        Utils.getApp().stopService(serviceIntent);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

# 引导到电池优化设置界面:

非常关键. 如果没有加入电池优化白名单,则拍照时,ContentObserver的onChange()没有任何回调

public class BatteryUtils {

    public static void requestIgnoreBatteryOptimizations() {
        PowerManager powerManager = (PowerManager) Utils.getApp().getSystemService(Context.POWER_SERVICE);

        // 防空指针保护
        if (powerManager == null) {
            return;
        }

        String packageName = Utils.getApp().getPackageName();

        // 1. 检查是否已经加入了白名单
        boolean isIgnoring = false;
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
            return;
        }
        isIgnoring = powerManager.isIgnoringBatteryOptimizations(packageName);

        if (!isIgnoring) {
            try {
                // 2. 尝试直接弹出“允许后台运行”的对话框
                Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                intent.setData(Uri.parse("package:" + packageName));

                // 如果传入的 context 不是 Activity (例如 ApplicationContext),需要加这个 Flag
                // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                ActivityUtils.getTopActivity().startActivity(intent);
            } catch (Exception e) {
                LogUtils.w(e);
                // 3. 兜底方案:如果上面的 Intent 报错,跳转到电池优化设置列表页
                try {
                    Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
                    // 同上,非 Activity Context 可能需要 Flag
                    // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    ActivityUtils.getTopActivity().startActivity(intent);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } else {
            // 已经在白名单里了,无需操作
            LogUtils.d("已经加入电量白名单");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
编辑 (opens new window)
一行代码完成startActivityForResult
android的activity布局玩法

← 一行代码完成startActivityForResult android的activity布局玩法→

最近更新
01
Android原生对flutter侧切换语言的适配
12-24
02
motion photo的压缩
10-10
03
360全景图的压缩
10-10
更多文章>
Theme by Vdoing | Copyright © 2020-2025 | 粤ICP备20041795号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式