使用service完成数据下载功能出现的问题

使用service完成数据下载功能出现的问题,第1张

   在Android启用service后台下载,使用异步线程进行下载,下载失败时,关掉service。在重新下载时,重新启动service,启动新的异步线程进行下载;因为上一次下载失败只是启动了停止了service,但上一个异步线程任务还在继续运行,此时又有新的线程任务运行,导致两个不同的线程访问同一段代码,导致出错。解决办法是,上一次下载失败时,除了关闭service也要cancel掉异步下载任务的request。

OkHttpClient mOkHttpClient = new OkHttpClient()

// 启动异步线程访问网络

public static void enqueue(Request request, Callback responseCallback, GetCallCancelCallback callback) {

Call call = mOkHttpClient.newCall(request)

callback.getCall(call)

call.enqueue(responseCallback)

}

一下是下载接口,传入下载的URL,存储路径,以及回调。

// 下载接口

public static void download(String downloadUrl, final String distPath, final DownloadCallback callback) {

Request request = new Request.Builder().url(downloadUrl).build()

enqueue(request, new Callback() {

@Override

public void onResponse(Response response) {

if (response.isSuccessful()) {

try {

InputStream is = response.body().byteStream()

long totalSize = response.body().contentLength()

FileOutputStream fos = new FileOutputStream(distPath)

int len = 0

long downloadSize = 0

byte[] buffer = new byte[1024 * 8]

while ((len = is.read(buffer)) != -1) {

downloadSize += len

callback.onDownload(downloadSize, totalSize)

fos.write(buffer, 0, len)

}

if (callback != null) {

callback.onSuccess(null)

return

}

} catch (IOException e) {

if (callback != null) {

callback.onFail(null)

}

}

}

if (callback != null) {

callback.onFail(null)

}

}

@Override

public void onFailure(Request request, IOException arg1) {

if (callback != null) {

callback.onFail(null)

}

}

}, new GetCallCancelCallback() {

@Override

public void getCall(Call call) {

callback.onGetCall(call) //回调,取得此时的Call

}

})

}

// service中的关键代码

download(url, filePath, new NetUtils.DownloadCallback() {

@Override

public void onSuccess(String result) {

downloadFinish()

}

@Override

public void onDownload(final long downloadSize, final long totalSize) {

mHandler.post(new Runnable() {

@Override

public void run() {

EventBus.getDefault().post(new DownloadProgressEvent(downloadSize / 1024, totalSize / 1024))

}

})

}

@Override

public void onFail(String errorInfo) {

//删掉该文件

LogUtils.error("onDowloadFail", "onDowloadFail")

File file = new File(filePath)

if (file.exists()) {

file.delete()

}

downloadFinish()

}

@Override

public void onTimeout() {

downloadFinish()

}

@Override

public void onGetCall(Call call) {

setCall(call)

}

})

// 关闭service,取消线程中的下载任务

private void downloadFinish() {

mHandler.post(new Runnable() {

@Override

public void run() {

isRunning = false

LogUtils.error("Service", mCall.toString())

if (mCall != null) {

mCall.cancel()// 取消当前的request

}

LogUtils.error("Service", "stop")

stopSelf()

}

})

Services

服务是一个应用程序组件,可以在后台执行长时间运行的操作,不提供用户界面。一个应用程序组件可以启动一个服务,它将继续在后台运行,即使用户切换到另一个应用程序。此外,一个组件可以绑定到一个服务与它交互,甚至执行进程间通信(IPC)。例如,一个服务可能处理网络通信,播放音乐,执行文件I/O,或与一个内容提供者交互,都在后台执行。

一个服务本质上讲有两种形式:

Started 启动的

started 形式的服务是指当一个应用组件(比如 activity )通过startService()方法开启的服务。一旦开启,该服务就可以 无限期 地在后台运行,哪怕开启它的组件被销毁掉。

通常,开启的服务执行一个单独的操作且并不向调用者返回一个结果。

比如,可能从网络进行下载或者上传一个文件。当任务完成,服务就该自我停止。

Bound 绑定的

bound 形式的服务是指一个应用组件通过调用 bindService() 方法与服务绑定。一个绑定的服务提供一个客户-服务端接口,以允许组件与服务交互,发送请求,获得结果,甚至执行进程间通信。一个绑定的服务只和与其绑定的组件同时运行。多个组件可以同时绑定到一个服务,但当全部接触绑定后,服务就被销毁。

虽然分这两类,但是一个服务可以同时使用这两种方式——可以用 started 无限期的运行,同时允许绑定。只需要在服务中实现两个回调方法: onStartCommand() 允许组件开启服务, onBind() 允许绑定。

不论应用程序是怎么起服务的, 任何 应用程序都可以用这个服务。同样的,任何组件可以使用一个 Activity 通过传递 Intent 开启服务。你也可以在配置文件设置服务为私有来防止其他应用访问该服务。

注意: 一个服务在进程中的主线程运行——一个服务 不会 创建自己的线程,也 不会 在另外的进程运行(除非另外指定)。这意味着,如果服务需要做一些频繁占用CPU的工作或者会发生阻塞的操作,你需要在服务中另开线程执行任务。这可以降低产生ANR的风险,提高用户体验。

基础

创建一个服务需要建立一个 Service 相关的子类,然后需要实现一些回调方法,好在不同的生命周期内做对应处理和绑定服务,比较重要的方法如下:

onStartCommand()

当其他组件,如 activity 请求服务启动时,系统会调用这个方法。一旦这个方法执行,服务就开始并且无限期的执行。如果实现这个方法,当这个服务完成任务后,需要你来调用 stopSelf() 或者 stopService() 停掉服务。如果只想提供绑定,不需要自己实现这个方法。

onBind()

当有其他组件想通过 bindService() 方法绑定这个服务时系统就会调用此方法。在实现的方法里面,必须添加一个供客户端使用的接口通过返回一个 IBinder 来与服务通信,这个方法必须实现。当然不想允许绑定的话,返回 null 即可。

onCreate()

服务第一次建立的时候会调用这个方法,执行一次性设置程序,在上面2个方法执行前调用。如果服务已存在,则不执行该方法。

onDestroy()

服务不再使用则使用该方法。服务应该实现这个方法来清理诸如线程,注册的监听器等资源。这是最后调用的方法。

安卓系统只会在内存占用很高,必须恢复系统资源供当前运行程序的情况下强制停掉一个运行中的服务。如果服务绑定在当前运行的程序中,就几乎不会被杀掉,如果服务声明了在前台运行(其实在后台,只是给系统一个错的信息来提高优先级),就几乎不会被杀掉。另外,如果一个服务正在运行,且运行了很久,系统就会根据运行时间把其排在后台任务列表的后面,则这个服务很容易被杀掉。根据onStartCommand() 的返回值设置,服务被杀掉后仍可以在资源充足的条件下立即重启。

是用一个服务好还是开一个线程好

一个服务就是一个可以忽略交互,在后台独立运行的组件,如果你需要这样就用服务

如果你需要在用户与程序交互时在主线程外执行任务,那就开个线程吧。

比如想播放音乐,但只在程序运行时播放,你可能在 onCreate() 开一个线程,在 onStart() 中开启它,在 onStop() 停止它。也可以考虑使用 AsyncTask 或者HandlerThread 取代一般的线程。

记住,如果使用一个服务,它还是默认在主线程中运行,如果会发生阻塞,还是要在服务中另开线程的。

在 manifest 文件声明服务

要使用服务就必须在 manifest 文件声明要用的所有服务,只用在<application>标签内添加子标签 <service>即可。

<manifest ...>

...

<application ...>

<service android:name=".ExampleService"

android:enabled=["true" | "false"]

android:exported=["true" | "false"]

android:isolatedProcess=["true" | "false"]

android:label="string resource"

android:icon="drawable resource"

android:permission="string"

android:process="string" >

...

</service>

</application>

</manifest>

下面对 service 标签属性做说明

android:name

你所编写的服务类的类名,可填写完整名称,包名+类名,如com.example.test.ServiceA ,也可以忽略包名,用 . 开头,如 .ServiceA,因为在 manifest 文件开头会定义包名,它会自己引用。

一旦你发布应用,你就不能改这个名字(除非设置 android:exported="false"),另外 name 没有默认值,必须定义。

android:enabled

是否可以被系统实例化,默认为 true

因为父标签 <application>也有 enable 属性,所以必须两个都为默认值true 的情况下服务才会被激活,否则不会激活。

android:exported

其他应用能否访问该服务,如果不能,则只有本应用或有相同用户ID的应用能访问。当然除了该属性也可以在下面 permission 中限制其他应用访问本服务。

这个默认值与服务是否包含意图过滤器 intent filters 有关。如果一个也没有则为 false

android:isolatedProcess

设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。与其通信的唯一途径是通过服务的API(binding and starting)。

android:label

可以显示给用户的服务名称。如果没设置,就用 <application>的 lable 。不管怎样,这个值是所有服务的意图过滤器的默认 lable 。定义尽量用对字符串资源的引用。

android:icon

类似 label ,是图标,尽量用 drawable 资源的引用定义。

android:permission

是一个实体必须要运行或绑定一个服务的权限。如果没有权限,startService() , bindService() 或 stopService() 方法将不执行,Intent 也不会传递到服务。

如果属性未设置,会由 <application>权限设置情况应用到服务。如果两者都未设置,服务就不受权限保护。

android:process

服务运行所在的进程名。通常为默认为应用程序所在的进程,与包名同名。<application>元素的属性 process 可以设置不同的进程名,当然组件也可设置自己的进程覆盖应用的设置。

如果名称设置为冒号 : 开头,一个对应用程序私有的新进程会在需要时和运行到这个进程时建立。如果名称为小写字母开头,服务会在一个相同名字的全局进程运行,如果有权限这样的话。这允许不同应用程序的组件可以分享一个进程,减少了资源的使用。

创建“启动的”服务

启动的(started)服务由 startService(Intent) 方法启动,在服务中的 onStartCommand()方法里获得 Intent 信息。关闭则由服务自己的方法 stopSelf() 或者由启动服务的地方调用 stopService(Intent) 方法来关闭。并不会因为启动服务的应用程序销毁而关闭。

示例,一个应用需要保存数据到远程数据库,这时启动一个服务,通过创建启动的服务给服务传递数据,由服务执行保存行为,行为结束再自我销毁。因为服务跟启动它的应用在一个进程的主线程中,所以对于耗时的操作要起一个新的线程去做。

//activity中

Intent intent = new Intent(MainActivity.this, ServiceA.class)

intent.putExtra("name", strName)

startService(intent)

//service中

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

// TODO Auto-generated method stub

// 获取数据

String strName = intent.getStringExtra("name")

// ... 数据库操作

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

耗时的操作

}

}).run()

return Service.START_STICKY

}

写服务有2种,继承 service 或者 IntentService 。后者是前者的子类。前者包含上面介绍的各种方法,用于普通的服务。后者可以自己开一个工作线程一个接一个处理多个请求。

继承IntentService

大多数服务不需要同时处理多个请求,继承 IntentService 是最好的选择

IntentService处理流程

创建默认的一个 worker 线程处理传递给 onStartCommand() 的所有 intent ,不占据应用的主线程

创建一个工作队列一次传递一个 intent 到你实现的 onHandleIntent() 方法,避免了多线程

在所以启动请求被处理后自动关闭服务,不需要调用 stopSelf()

默认提供 onBind() 的实现,并返回 null

默认提供 onStartCommand() 的实现,实现发送 intent 到工作队列再到你的onHandleIntent() 方法实现。

这些都加入到 IntentService 中了,你需要做的就是实现构造方法和onHandleIntent() ,如下:

public class HelloIntentService extends IntentService {

/**

* A constructor is required, and must call the super IntentService(String)

* constructor with a name for the worker thread.

*/

public HelloIntentService() {

super("HelloIntentService")

}

/**

* The IntentService calls this method from the default worker thread with

* the intent that started the service. When this method returns, IntentService

* stops the service, as appropriate.

*/

@Override

protected void onHandleIntent(Intent intent) {

// Normally we would do some work here, like download a file.

// For our sample, we just sleep for 5 seconds.

long endTime = System.currentTimeMillis() + 5*1000

while (System.currentTimeMillis() <endTime) {

synchronized (this) {

try {

wait(endTime - System.currentTimeMillis())

} catch (Exception e) {

}

}

}

}

}

如果需要重写其他回调方法,如 onCreate() , onStartCommand() 等,一定要调用super() 方法,保证 IntentService 正确处理 worker 线程,只有 onHandleIntent()和 onBind() 不需要这样。如:

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()

return super.onStartCommand(intent,flags,startId)

}

继承Service

继承 Service 就可以实现对请求多线程的处理,前面介绍了 service 的生命周期,可以按照生命周期实现方法。就不放示例了。

onStartCommand() 的返回值

返回一个整型值,用来描述系统在杀掉服务后是否要继续启动服务,返回值有三种:

START_NOT_STICKY

系统不重新创建服务,除非有将要传递来的 intent 。这是最安全的选项,可以避免在不必要的时候运行服务。

START_STICKY

系统重新创建服务并且调用 onStartCommand() 方法,但并不会传递最后一次传递的 intent ,只是传递一个空的 intent 。除非存在将要传递来的 intent ,那么就会传递这些 intent 。这个适合播放器一类的服务,不需要执行命令,只需要独自运行,等待任务。

START_REDELIVER_INTENT

系统重新创建服务并且调用 onStartCommand() 方法,传递最后一次传递的intent 。其余存在的需要传递的intent会按顺序传递进来。这适合像下载一样的服务,立即恢复,积极执行。

如果想从服务获得结果,可以用广播来处理

创建“绑定的”服务

用 bindService() 方法将应用组件绑定到服务,建立一个长时间保持的联系。

如果需要在 activity 或其他组件和服务交互或者通过进程间通信给其他应用程序提供本应用的功能,就需要绑定的服务。

建立一个绑定的服务需要实现 onBind() 方法返回一个定义了与服务通信接口的IBinder 对象。其他应用程序组件可以调用 bindService() 方法获取接口并且调用服务上的方法。

创建一个绑定的服务,第一件事就是定义一个说明客户端与服务通信方式的接口。这个接口必须是 IBinder 的实现,并且必须要从 onBind() 方法返回。一旦客户端接收到了 IBinder ,就可以通过这个接口进行交互。

多个客户端可以绑定到一个服务,可以用 unbindService() 方法解除绑定,当没有组件绑定在服务上,这个服务就会被销毁。

//activity中

private ServiceConnection connB = new ServiceConnection() {

@Override

public void onServiceDisconnected(ComponentName name) {

// TODO Auto-generated method stub

Log.v(tag, "Service B disconnected")

}

@Override

public void onServiceConnected(ComponentName name, IBinder service) {

// TODO Auto-generated method stub

Log.v(tag, "Service B connected")

MyBinderB binder = (MyBinderB) service

ServiceB SB = binder.getService()

SB.showLog()

}

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

findViewById(R.id.button1).setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

// TODO Auto-generated method stub

Intent a = new Intent(MainActivity.this, ServiceB.class)

bindService(a, connB, BIND_AUTO_CREATE)

}

}

//ServiceB

public class ServiceB extends Service {

public void showLog() {

Log.i(tag, "serviceB-->showLog()")

}

public class MyBinderB extends Binder {

public ServiceB getService() {

return ServiceB.this

}

}

private MyBinderB myBinderB = new MyBinderB()

@Override

public IBinder onBind(Intent intent) {

// TODO Auto-generated method stub

return myBinderB

}

}

启动前台服务

前台服务是被认为是用户已知的正在运行的服务,当系统需要释放内存时不会优先杀掉该进程。前台进程必须发一个 notification 在状态栏中显示,直到进程被杀死。因为前台服务会一直消耗一部分资源,但不像一般服务那样会在需要的时候被杀掉,所以为了能节约资源,保护电池寿命,一定要在建前台服务的时候发notification ,提示用户。当然,系统提供的方法就是必须有 notification 参数的,所以不要想着怎么把 notification 隐藏掉。

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

// TODO Auto-generated method stub

Intent notificationIntent = new Intent(this, MainActivity.class)

PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)

Notification noti = new Notification.Builder(this)

.setContentTitle("Title")

.setContentText("Message")

.setSmallIcon(R.drawable.ic_launcher)

.setContentIntent(pendingIntent)

.build()

startForeground(12346, noti)

return Service.START_STICKY

}

startForeground() 方法就是将服务设为前台服务。参数12346就是这个通知唯一的id,只要不为0即可。

1、Alerter

Alerter(警示器)服务的进程名是Service.exe(即启动这个服务后在后台运行的进程名

称,可以通过任务管理器看到,下同)。Alerter服务的功能是,WinXP将系统上发生的与

管理有关的事件以警示(Alert)信息传送至网络上指定的电脑或用户,例如当发生打印错

误或硬盘即将写满等事件,这类警示信息由WinXP的警示器服务(Alerter Service)收集

、送出。尽管Alerter依存的服务并没有Messenger(信使)服务,但Alerter服务必须依靠

后者才能送出信息,故在启动Alerter服务后还必须确定Messenger服务也在工作状态,而

接收的电脑也必须启动Messenger服务。由于Alerter服务运行后,服务是用户可以发送“

弹出(Pop-up)”信息给其他用户,这些信息有可能被攻击者用来实施攻击,如诱骗用户

修改口令等,从而造成安全隐患。同时该服务使得用户账号泄漏,也有可能被攻击者利用

来进行口令猜测攻击。所以对于家庭单机用户,甚至对于绝大多数小型的局域网来说,这

个功能是完全可禁用的,不仅节省了系统资源和加快启动速度,也提高了机器的安全性。

2、Application Layer Gateway Service

简称“ALG”(应用层网关)的进程名是alg.exe,WinXP Home/Pro默认安装的启动类型为

手动。ALG又被称为代理服务器(Proxy Server),是网络防火墙从功能层面上分类的一种

。当内部计算机与外部主机连接时,将由代理服务器担任内部计算机与外部主机的连接中

继者。使用ALG的好处是隐藏内部主机的地址和防止外部不正常的连接,如果代理服务器上

未安装针对该应用程序设计的代理程序时,任何属于这个网络服务的封包将无法通过防火

墙。通俗点说,具体到ALG本身,它就是附带的Internet连接共享/防火墙的具体控管程序

,如果你需要启动这二者,这个服务是必备的。当然,只有一台计算机的上网家庭可以考

虑禁用这个服务,不过WinXP内置的防火墙效果还是不错的,如果不是坚持要用第三方的防

火墙,还是开着它吧。

3、Application Management

AppMent(应用程序管理服务)的进程名是Svchost.exe,WinXP Home/Pro默认安装的启动

类型为手动,没有任何依存服务关系。从Win2000开始,微软引入了一种基于MSI文件格式

(应用程序安装信息程序包文件)的全新、有效软件管理方案——即应用程序管理组件服

务(Application Management),它不仅管理软件的安装、删除,而且可使用此项服务修

改、修复现有应用程序,监视文件复原并通过复员排除基本故障等。通常这个服务我们保

持其默认状态较好。

可能许多朋友都有印象,当年ACDSee4.0刚发布时,由于安装制作上的考虑不周,并没有考

虑到那个时候大多数人的系统还并不支持MSI安装格式,结果只得又去下载安装一个名为W

indows Installer的MSI辅助文件才解决问题。通常以MSI文件格式安装的软件十分好认,

比如说Office XP,当你安装后再次运行软件的安装程序时,它一般会有“重新安装”、“

修复软件”、“卸载软件”多个选项,而不是以前安装程序那种就简单地卸载或覆盖安装

了事。

4、Automatic Updates

Wuauserv(自动更新服务)的进程名是Svchost.exe,WinXP Home/Pro默认安装的启动类型

为自动,没有任何依存服务关系。这个是大家都非常熟悉的系统自动更新功能,就不多说

了。用小猫上网而深受其苦的朋友记得在系统属性中关闭是不够的,还要将Automatic Up

dates这个服务禁用才可以。以后需要更新,直接在IE中输入Windows Update网站地址htt

p://v4.windowsupdate.microsoft.com/zhcn/default.asp手动更新即可。

5、Background Intelligent Transfer Service

BITS(后台智能传输服务)的进程名是Svchost.exe,WinXP Home/Pro默认安装的启动类型

为手动,依赖于Remote Procedure Call、Workstation服务。微软宣称BITS能够利用剩余

的带宽传输文件,当网络切断或计算机重启时,后台智能传输服务会自动对文件传输加以

维护,当网络重新连接时,后台智能传输服务将从停止的地方继续开始传输文件。其实这

个服务原是用来实现HTTP1.1服务器之间的信息传输,基本上它的应用也就是支持自动更新

时的断点续传。如果你禁用了Automatic Updates,留着它也没什么意义。

6、ClipBook

ClipSrv(剪贴板查看器服务)的进程名是clipsrv.exe,WinXP Home/Pro默认安装的启动

类型为手动,依赖于Network DDE服务。ClipBook通过Network DDE和Network DDE DSDM提

供的网络动态数据交换服务,可查阅远程机器中的剪贴版,通俗地说ClipBook就是支持剪

贴版查看器(ClipBook Viewer)程序,该程序可允许剪贴页被远程计算机上的ClipBook浏

览。

例如有个较大的文档工程,由A、B、C共同开发。A负责Excel数据部分,B负责Visio制图部

分,而C负责将两部分文档整合。C经常需要对A、B的数据进行拷贝,愚蠢的做法是C打开A

、B在网络邻居上共享的文档,然后将相关内容拷贝。而对Windows体系有一定了解的用户

应该听说过OLE这个东西,上面说的Excel数据和Visio制图都可以认为是独立的OLE对象,

如果A、B、C的3台机器上的ClipBook服务都为开启,就可利用ClipBook共享这些OLE对象,

C只要在自己的文档中建立OLE对象的链接指向A、B的Excel和Visio,A、B对自己工作的任

何改动即可在C的复合文档里自动体现。由此可见,ClipBook是基于对象的共享,而非简单

的文件共享。所以也很好理解,这是一把双刃剑,在带来极大方便的同时,也带来被非法

远程访问ClipBook剪贴页面的安全隐患。对于没有上述类似工作,又不准备使用或极少使

用远程桌面的用户,这个服务完全可以禁用,在需要的时候在打开。

7、COM+ Event ***

Event *** (COM+事件系统服务)的进程名是Svchost.exe,WinXP Home/Pro默认安装的启

动类型为手动,依赖于Remote Procedure Call服务。对于非软件开发专业的朋友来说,C

OM+是个非常难理解的名词。简单地说COM+是一种软件构件/组件的标准。比如写一个软件

好比是盖一座房子,而门窗等部件会根据标准设计,以求得省时省力。COM组件即是Windo

ws的门窗等标准组件了,COM+是对COM的进一步扩展,其具体含义在此就不详细介绍了。W

indows系统又是个典型的消息(事件)处理型系统,很多功能都是由消息来触发的,这就

产生了COM+ Event *** 。我们要学习的是如何简单判断自己的系统中是否有程序依靠此服

务。检查你的系统安装盘下的“Program files\ComPlus Applications”目录,如果没有

东西就可以把这个服务关闭了。

8、COM+ *** Application

COMSysApp(COM+系统应用服务)的进程名是Dllhost.exe,WinXP Home/Pro默认安装的启

动类型为手动,依赖于Remote Procedure Call服务。简单地说,COM+ *** Application是

COM+ Event *** 的具体执行者,如果禁用了COM+ Event *** 也就自然禁用它。

9、Computer Browser

Browser(计算机浏览器服务)的进程名是Svchost.exe,WinXP Home/Pro默认安装的启动

类型为自动,依赖于Server和Workstation服务。Browser服务维护着一个网络资源的清单

,其中包括基于Windows的域、工作组和计算机,还有其他支持NetBIOS协议的网络设备,

我们在“网上邻居”上看到显示的内容正是来源于此。显然对于一般家庭用的计算机这个

服务并不需要,除非计算机位于局域网上,例如用长城宽带的朋友,用它可方便地知道社

区内的网络环境。这个服务还是慎重对待较好,若不是太在意还是将其设置为自动吧。


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/469315.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-06-05
下一篇2023-06-05

发表评论

登录后才能评论

评论列表(0条)

    保存