王学岗性能优化9——电量优化

    xiaoxiao2022-06-24  204

    第一:电量和网络状态的监控 因为本章涉及到粘性广播,所以我这里有介绍下粘性广播的一篇文章粘性广播 Battery类

    package com.example.administrator.lsn_9_demo; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.BatteryManager; /** * 主动得到充电状态和网络状态 */ public class Battery { /** * 是否在充电 */ public static boolean isPlugged(Context context){ //1,发送一个包含充电状态的广播,并用粘性广播、 //2,电量发生改变的一个广播 IntentFilter filter=new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent intent = context.registerReceiver(null, filter); //获取充电状态,-1为默认值 int isPlugged=intent.getIntExtra(BatteryManager.EXTRA_PLUGGED,-1); //电源充电 boolean acPlugged=isPlugged==BatteryManager.BATTERY_PLUGGED_AC; boolean usbPlugged=isPlugged==BatteryManager.BATTERY_PLUGGED_USB; //无线充电 boolean wrieliessPlugged=isPlugged==BatteryManager.BATTERY_PLUGGED_WIRELESS; return acPlugged || usbPlugged || wrieliessPlugged; } /** * 是否在使用wifi */ public static boolean isWifi(Context context){ ConnectivityManager cm=(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); //获取当前活动的网络信息 NetworkInfo activeNetworkInfo=cm.getActiveNetworkInfo(); //活动信息是存在的;是否断网;是不是wifi if(null!=activeNetworkInfo && activeNetworkInfo.isConnected() && activeNetworkInfo.getType()==ConnectivityManager.TYPE_WIFI){ return true; } return false; } }

    以上代码可以判断网络存在还是不存在,以及充电状态。这种方法时主动通过代码来访问的,还有一种方式就是我们可以通过广播来处理。当我们开始充电或者切换网络的时候,系统本身也会发送一个广播, 注册电源广播

    <receiver android:name=".PowerConnectionReceiver"> <intent-filter> <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/> <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/> </intent-filter>

    WiFi广播

    <receiver android:name=".WifiConnectionReceiver"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> </intent-filter> </receiver> package com.dongnao.battery; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.Toast; /** * 被动获取是否wifi */ public class WifiConnectionReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Battery.isWifi(context)){ Log.i("WIFI","当前正在使用wifi"); Toast.makeText(context,"当前正在使用wifi",Toast.LENGTH_SHORT).show(); } else{ Log.i("WIFI","当前不正在使用wifi"); Toast.makeText(context,"当前不在使用wifi",Toast.LENGTH_SHORT).show(); } } } package com.dongnao.battery; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.text.TextUtils; import android.widget.Toast; /** * 被动接收充电状态,收到广播之后就知道是否在充电了 */ public class PowerConnectionReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (TextUtils.equals(action,Intent.ACTION_POWER_CONNECTED)){ Toast.makeText(context,"当前正在充电",Toast.LENGTH_SHORT).show(); } else if(TextUtils.equals(action,Intent.ACTION_POWER_DISCONNECTED)){ Toast.makeText(context,"当前不在充电",Toast.LENGTH_SHORT).show(); } } }

    第二:解决即时性很高的需求,以高德地图为例 解决即时性很高的需求,比如股票软件,时时刻刻会接受信息,再比如地图,要不断收集信息,本例子中就是每隔五秒,就会有 一个地理信息发过来。 我们这里有个LocationManager类,具体的定位类,完全是仿照高德地图写的,没什么可以讲的,

    package com.dongnao.battery.location; import android.content.Context; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import com.amap.api.location.AMapLocation; import com.amap.api.location.AMapLocationClient; import com.amap.api.location.AMapLocationClientOption; import com.amap.api.location.AMapLocationListener; import com.dongnao.battery.JobManager; public class LocationManager { //声明AMapLocationClient类对象 private AMapLocationClient mLocationClient; private static LocationManager instance; private Context applicationContext; public LocationManager() { } public static LocationManager getInstance() { if (null == instance) { instance = new LocationManager(); } return instance; } //1,声明定位回调监听器, //2,如果位置发生改变,会把aMapLocation地址传过来 //传过来之后会调用 ,UploadService.UploadLocation(applicationContext, location);把位置信息上传到 //服务器 public AMapLocationListener mLocationListener = new AMapLocationListener() { @Override public void onLocationChanged(AMapLocation aMapLocation) { if (aMapLocation != null) { if (aMapLocation.getErrorCode() == 0) { //获得json String location = aMapLocation.toStr(); //启动IntentService 上传坐标数据 // 主要不是实时需要的,延迟执行 UploadService.UploadLocation(applicationContext, location); } else { //定位失败时,可通过ErrCode(错误码)信息来确定失败的原因,errInfo是错误信息,详见错误码表。 Log.e("AmapError", "location Error, ErrCode:" + aMapLocation.getErrorCode() + ", errInfo:" + aMapLocation.getErrorInfo()); } } } }; public void startLocation(Context context) { if (null != mLocationClient) { mLocationClient.startLocation(); return; } applicationContext = context.getApplicationContext(); //初始化定位 mLocationClient = new AMapLocationClient(applicationContext); //设置定位回调监听 mLocationClient.setLocationListener(mLocationListener); //声明AMapLocationClientOption对象 AMapLocationClientOption mLocationOption = null; //初始化AMapLocationClientOption对象 mLocationOption = new AMapLocationClientOption(); mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); //设置定位间隔,单位毫秒,默认为2000ms,最低1000ms。 mLocationOption.setInterval(5000); //设置是否返回地址信息(默认返回地址信息) mLocationOption.setNeedAddress(true); //设置是否允许模拟位置,默认为true,允许模拟位置 mLocationOption.setMockEnable(true); //单位是毫秒,默认30000毫秒,建议超时时间不要低于8000毫秒。 mLocationOption.setHttpTimeOut(20000); //关闭缓存机制 mLocationOption.setLocationCacheEnable(false); //给定位客户端对象设置定位参数 mLocationClient.setLocationOption(mLocationOption); //启动定位 mLocationClient.startLocation(); } public void stopLocation() { if (null != mLocationClient) { mLocationClient.stopLocation();//停止定位后,本地定位服务并不会被销毁 } } public void destoryLocation() { if (null != mLocationClient) { mLocationClient.unRegisterLocationListener(mLocationListener); mLocationClient.onDestroy();//销毁定位客户端,同时销毁本地定位服务。 mLocationClient = null; } } }

    把定位信息上传到服务器的类 上传服务我写了一个UploadLocation方法,开启当前服务,该服务一旦开启,会自动调用onHandleIntent()方法。在onHandleIntent()方法中,我会把从网上得到的坐标信息通过一个流发送到服务器

    package com.dongnao.battery.location; import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.support.annotation.Nullable; import android.util.Log; import com.dongnao.battery.Utils; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; /** *定位以后把定位信息上传到服务器 */ public class UploadService extends IntentService { public UploadService() { super("upload Location"); } //开启当前服务 public static void UploadLocation(Context context, String location) { Intent intent = new Intent(context, UploadService.class); intent.putExtra("DATA", location); context.startService(intent); } //1,IntentService 开启后会自动调用该方法 //2,从网络上得到的坐标信息(location)通过一个流发送 //到一个url服务器上(https://www.baidu.com/备注:这个路径是我胡乱写的) @Override protected void onHandleIntent(@Nullable Intent intent) { String location = intent.getStringExtra("DATA"); Log.i("dongnao", "IntentService 获得了位置信息:" + location); HttpURLConnection conn = null; OutputStream os = null; try { conn = (HttpURLConnection) new URL("https://www.baidu.com/") .openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); os = conn.getOutputStream(); os.write(location.getBytes()); os.flush(); Log.i("dongnao", "IntentService 上传位置信息"); } catch (IOException e) { e.printStackTrace(); } finally { Utils.safeColose(os); if (null != conn) { conn.disconnect(); } } } }

    这个类是干货,这个类是定位服务,有三种方式,一个是WakeLock(本例中被注释的代码用的就是WakeLock),定义WakeLock保证后台代码长时间运行,这个时候就可以长时间的收到信息,但这种方式比较耗电,所以我们还有第二种方式和第三种方式。第二种方式就是闹钟的方式,使用闹钟就不会长期占用CPU

    package com.dongnao.battery.location; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.app.job.JobScheduler; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.IBinder; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; import android.support.annotation.Nullable; import android.text.TextUtils; import com.dongnao.battery.JobManager; public class LocationService extends Service { //使用WakeLock保证后台代码长时间的一直执行,但比较耗电,现在被android弃用 private PowerManager.WakeLock locationLock; //使用闹钟需要一个intent private Intent alarmIntent; @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //使用WeakLock,拿到电源管理服务 // PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); //判断是否支持 // pm.isWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK); //只唤醒cpu // locationLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, // "location_lock"); // locationLock.acquire(); //创建一个闹钟 alarmKeep(); } @Override public void onDestroy() { super.onDestroy(); //释放 LocationManager.getInstance().destoryLocation(); //不用的时候注销广播接收者 unregisterReceiver(alarmReceiver); // if (null != locationLock) { // locationLock.release(); // } } //1,用来解决即时性很高的需求,比如每隔五秒钟来做一次事情,我们可以使用闹钟, //闹钟可以间歇性的使用CPU,比如本应用就是每隔5秒钟使用一次cpu,这样就不会在后台一直运行, //但是如果我们使用WeakLock,即使屏幕关掉了,cpu也会继续工作,这样就会比较耗电 private void alarmKeep() { alarmIntent = new Intent(); //Location是自己随便定义的 alarmIntent.setAction("LOCATION"); //创建延迟意图,心跳也可以使用闹钟 PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, alarmIntent, 0); //获得闹钟管理器 AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); //动态注册广播接受者,来进行闹钟的操作 IntentFilter filter = new IntentFilter(); //Location是自己随便定义的 filter.addAction("LOCATION"); registerReceiver(alarmReceiver,filter); //每隔 5s 发送一个广播 alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME,SystemClock.elapsedRealtime(), 5_000,broadcast); } BroadcastReceiver alarmReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //如果intent.getAction()是LOCATION if (TextUtils.equals(intent.getAction(),"LOCATION")){ LocationManager.getInstance().startLocation(LocationService.this); } } }; }

    locationService,为定位的类,定位以后通过UploadService上传定位信息 文件清单中注册两个类,单独开启一个进程

    <service android:name=".location.LocationService" android:process=":location" /> <service android:name=".location.UploadService" android:process=":location" />

    第三jobScheduler 推送就可以使用jobScheduler,使用jobScheduler可以定时的发送一些东西 MyJobService类

    package com.dongnao.battery; import android.app.job.JobParameters; import android.app.job.JobService; import android.os.AsyncTask; import android.os.PersistableBundle; import android.util.Log; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; /** * 1,将来所有的事情,比如上传信息,就通过这个服务来完成,不在使用UploadService了。 * 2,要在功能清单中注册,但开一个进程 * <service * android:name=".MyJobService" * android:permission="android.permission.BIND_JOB_SERVICE" * android:process=":location" /> */ public class MyJobService extends JobService { public static final String TAG = "MyJobService"; /** * * @param params 1,我们通过JobScheduler执行任我的时候,任务中的信息可以通过该参数得到 * 2,工作在执行的时候,它身上各种信息都在params里 * @return */ @Override public boolean onStartJob(JobParameters params) { //当任务执行完毕时你需要调用jobFinished(JobParameters params, boolean needsRescheduled)来通知系统 new MyAsyncTask().execute(params);//提交到系统中这些服务的参数先拿到 //1,如果返回值是false,这个方法返回时任务已经执行完毕。就不会去执行onStopJob方法 //2,如果返回值是true,那么这个任务正要被执行,我们就需要开始执行任务。 //要手动调用jobFinished()来通知系统。 return true; } //当系统接收到一个取消请求时 @Override public boolean onStopJob(JobParameters params) { //如果onStartJob返回false,那么onStopJob不会被调用 //如果onStopJob()返回 true 则会重新计划这个job,很少会返回true,一般都是返回false return false; } /** * 最好单独写一个文件,这里我就写到一起了 * Params:启动任务时输入的参数类型. * <p> * Progress:后台任务执行中返回进度值的类型. * <p> * Result:后台任务执行完成后返回结果的类型. */ class MyAsyncTask extends AsyncTask<JobParameters, Void, Void> { JobParameters jobParameters; @Override protected Void doInBackground(JobParameters[] objects) { jobParameters = objects[0]; Log.i(TAG, jobParameters.getJobId() + " 任务开始执行......"); //拿到包裹 PersistableBundle extras = jobParameters.getExtras(); //获取包裹中的定位信息 String location = extras.getString("DATA"); Log.i(TAG, jobParameters.getJobId() + " 上传:" + location); HttpURLConnection conn = null; OutputStream os = null; try { //服务器路径随便填写 conn = (HttpURLConnection) new URL("https://www.baidu.com/") .openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); os = conn.getOutputStream(); os.write(location.getBytes()); os.flush(); } catch (IOException e) { e.printStackTrace(); } finally { Utils.safeColose(os); if (null != conn) { conn.disconnect(); } } return null; } /** * doInBackground:必须重写,异步执行后台线程要完成的任务,耗时操作将在此方法中完成. * <p> * onPreExecute:执行后台耗时操作前被调用,通常用于进行初始化操作. * <p> * onPostExecute:当doInBackground方法完成后,系统将自动调用此方法,并将doInBackground方法返回的值传入此方法.通过此方法进行UI的更新. * <p> * onProgressUpdate:当在doInBackground方法中调用publishProgress方法更新任务执行进度后,将调用此方法.通过此方法我们可以知晓任务的完成进度. */ @Override protected void onPostExecute(Void s) { //1,因为onStartJob返回的是true,所以当任务执行完毕之后,需要调用 // jobFinished来让系统知道这个任务已经结束,如果是false,就不需要执行这个代码 //2,系统可以将下一个任务添加到队列中 //3,参数true表示需要重复执行,false反之,很少写true,绝大部分情况下都是false jobFinished(jobParameters, false); Log.i(TAG, jobParameters.getJobId() + "任务执行完成......"); } } }

    JobManager类

    package com.dongnao.battery; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.os.Build; import android.os.PersistableBundle; import java.util.List; //各种服务性操作,google建议用它替换掉普通的服务 public class JobManager { static JobManager instance; //把一些不是特别紧急(实时)的任务放到更合适的时机批量处理 // 好处1、避免频繁的唤醒硬件模块,比如网络模块等 // 好处2、避免在不合适的时候执行一些耗电的任务,比如我们第二部分这个代码 // alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME,SystemClock.elapsedRealtime(), // 5_000,broadcast); //每隔5秒就接收到坐标,就会把这个坐标发送到服务器,假设现在我要统计一个人一天的行程,这样的发送 //显然没有必要。 像这样的需求我们可以一次整合很多内容在一次性的发送。把很多工作流合并在一起,起到 //一个合并的作用。 private JobScheduler jobScheduler; private Context context; private static final int jobId=0; //单例模式,省去了非空判断,自己加上 public static JobManager getInstance() { if (null == instance) instance = new JobManager(); return instance; } //初始化工作流 //在Application的onCreate()中初始化 public void init(Context context) { this.context = context.getApplicationContext(); //jobScheduler本身就是一个系统服务 //所有系统服务的初始化都有两种,一种是通过aidl进程间通讯,另外一种就是反射 jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); } /** * 添加一个任务,把服务器传回来的信息一个个添加进来 * 比如每五秒收到一次坐标,我要把这些坐标任务全部添加在一起,再统一去延迟执行 * @param location */ //把服务器传回来的信息一个个添加进来 public void addJob(String location) { if (null == jobScheduler) { return; } //1,jobinfo中会用来存放工作流相关的信息 //2, 当我们添加任我的时候需要JobInfo JobInfo pendingJob = null; //整合多个job,Build.VERSION_CODES.N(android 7.0)以上和以下分两种情况处理。 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //1,查找id是0(即jobId)的job,id号是由用户决定的。 //2, 等下我们在添加的时候,会先到已经有的一个工作队列(比如 //我们有10个工作,jobScheduler会拿一个集合存放,这个集合就是工作队列),在我们添加id为0的任务之前, //有可能已经有很多任务在这个工作队列里面了。 //3,工作队列第一次肯定是空的,非第一次就未必,这句代码是查找先前的工作队列中有没有内容 pendingJob = jobScheduler.getPendingJob(jobId); } else { List<JobInfo> allPendingJobs = jobScheduler.getAllPendingJobs(); for (JobInfo info : allPendingJobs) { //查找IDhao相同的工作/任务 if (info.getId() == jobId) { pendingJob = info; break; } } } //找到待执行的job if (null != pendingJob) { //1,多个坐标信息拼到一起上传 //2,android 5.0以后才可以使用JobScheduler if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //1,与Intent一样,用来存数据 //1,PersistableBundle就是存东西的对象,与我们平时用的bundle没啥区别 PersistableBundle extras = pendingJob.getExtras(); //获得上一次设置的location数据 String data = extras.getString("DATA"); //1,如果有多条就自定义,这个连接符要和服务器商量,比如 多条坐标数据用@隔开 //2,每5秒就会拼接一次,只要有任务就拼接字符串 location = data + "@" + location; //这次任务完成了 jobScheduler.cancel(jobId); } } //1, jobid :0 //2,创建任务 PersistableBundle extras = new PersistableBundle(); extras.putString("DATA",location); //1,每来一个job,创建一个job //2,添加任务的时候需要一个叫JobInfo的内容,有了这个内容后可以把地图坐标信息, //3,把地图信息放到extras里,就可以生成JobInfo JobInfo jobInfo = new // 把某个工作加进来,jobID是加进来工作的ID JobInfo.Builder(jobId, //1,把jobservice绑定到jobInfo上面,执行的时候服务就会被跑起来,MyJobService就是需要执行的服务 //2,MyJobService会调用很多次,调用一次就会拼接一次Location,这样的话就会有大量的工作排好队 //这些工作什么时候执行呢?充电和链接wifi的时候就开始执行任务,就开始上传。调用 //jobScheduler.schedule(jobInfo);后,刚刚排队的任务就会一个一个跑,每个任务跑都会拼成 //location@的样子。服务往服务器提交(在MyJobService的onStartJob()里写)之前,这些东西 //会链接在一起。 //3,把拼接的东西加入工作队列 new ComponentName(context, MyJobService.class)) //是么时候执行jobservice,只在充电的时候执行 .setRequiresCharging(true) //不是蜂窝网络,就是使用wifi .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setExtras(extras).build(); //1,提交任务 //2,id为0的服务会全部累计在一个队列里面,提交任务完成后, // 满足充电无线网这两个条件的时候,所有id号为0的这一组服务,就会按照特定的优先级, // 一个一个的去执行。 jobScheduler.schedule(jobInfo); } }

    在LocationManager中调用

    package com.dongnao.battery.location; import android.content.Context; import android.util.Log; import com.amap.api.location.AMapLocation; import com.amap.api.location.AMapLocationClient; import com.amap.api.location.AMapLocationClientOption; import com.amap.api.location.AMapLocationListener; import com.dongnao.battery.JobManager; public class LocationManager { //声明AMapLocationClient类对象 private AMapLocationClient mLocationClient; private static LocationManager instance; private Context applicationContext; public LocationManager() { } public static LocationManager getInstance() { if (null == instance) { instance = new LocationManager(); } return instance; } //声明定位回调监听器 public AMapLocationListener mLocationListener = new AMapLocationListener() { @Override public void onLocationChanged(AMapLocation aMapLocation) { if (aMapLocation != null) { if (aMapLocation.getErrorCode() == 0) { //获得json String location = aMapLocation.toStr(); //启动IntentService 上传坐标数据 // 主要不是实时需要的,延迟执行 // UploadService.UploadLocation(applicationContext, location); //每隔5秒会执行一次 JobManager.getInstance().addJob(location); } else { //定位失败时,可通过ErrCode(错误码)信息来确定失败的原因,errInfo是错误信息,详见错误码表。 Log.e("AmapError", "location Error, ErrCode:" + aMapLocation.getErrorCode() + ", errInfo:" + aMapLocation.getErrorInfo()); } } } }; public void startLocation(Context context) { if (null != mLocationClient) { mLocationClient.startLocation(); return; } applicationContext = context.getApplicationContext(); //初始化定位 mLocationClient = new AMapLocationClient(applicationContext); //设置定位回调监听 mLocationClient.setLocationListener(mLocationListener); //声明AMapLocationClientOption对象 AMapLocationClientOption mLocationOption = null; //初始化AMapLocationClientOption对象 mLocationOption = new AMapLocationClientOption(); mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); //设置定位间隔,单位毫秒,默认为2000ms,最低1000ms。 mLocationOption.setInterval(5000); //设置是否返回地址信息(默认返回地址信息) mLocationOption.setNeedAddress(true); //设置是否允许模拟位置,默认为true,允许模拟位置 mLocationOption.setMockEnable(true); //单位是毫秒,默认30000毫秒,建议超时时间不要低于8000毫秒。 mLocationOption.setHttpTimeOut(20000); //关闭缓存机制 mLocationOption.setLocationCacheEnable(false); //给定位客户端对象设置定位参数 mLocationClient.setLocationOption(mLocationOption); //启动定位 mLocationClient.startLocation(); } public void stopLocation() { if (null != mLocationClient) { mLocationClient.stopLocation();//停止定位后,本地定位服务并不会被销毁 } } public void destoryLocation() { if (null != mLocationClient) { mLocationClient.unRegisterLocationListener(mLocationListener); mLocationClient.onDestroy();//销毁定位客户端,同时销毁本地定位服务。 mLocationClient = null; } } }

    在这里调用

    //每隔五秒添加一个服务,但这个服务并不是5秒就执行,会把这些服务全部累计在一个队列里面 JobManager.getInstance().addJob(location);

    在Application的onCreate()中初始化

    JobManager.getInstance().init(this);

    我们测试发现wakelock变成小黄点了,不再是一条直线


    最新回复(0)