Android中实现多线程操作的几种方式
目录
- 前言
- 最基础的方式
- 继承Thread类并实现run()方法
- 匿名内部类
- 实现Runnable接口
- callable+FutureTask
- 线程池
- 手动创建线程池
- 使用Executors创建线程池
- Android中特有的实现多线程
- 使用HandlerThread
- 使用IntentService
- JobIntentService/JobScheduler
- WorkManager
- WorkManager
- 使用协程
- AsyncTask
- 结语
前言
多线程一直是一个老大难的问题,首先因为它难以理解,其次在实际工作中我们需要面对的关于线程安全问题也并不常见,今天就来总结一下实现多线程的几种方式,可能并不全面,还请各位看官多多补充。
最基础的方式
继承Thread类并实现run()方法
class MyThread extends Thread{ @Override public void run() { System.out.println("我是子线程"); } } new MyThread().start();
匿名内部类
public class ManyThread { public static void main(String[] args) { new Thread(){ public void run() { System.out.println("我是子线程"); }; }.start(); } }
实现Runnable接口
class MyRunnable implements Runnable{ @Override public void run() { System.out.println("我是子线程"); } } Thread thread = new Thread(new MyRunnable()); thread.start();
太简单了吧!!别着急,这只是热身~
callable+FutureTask
Callable接口类似于Runnable,区别在于使用callable可以拿到结果。
FutureTask实现了RunnableFuture,
public class FutureTask<V> implements RunnableFuture<V> { ... }
RunnableFuture又实现了Runnable和Future
public interface RunnableFuture<V> extends Runnable, Future<V> { /** * Sets this Future to the result of its computation * unless it has been cancelled. */ void run(); }
所以FutureTask既可以当Runnable使用又可以当Future使用get()来拿到结果
future的API:get():当任务结束后,返回一个结果,如果调用时,任务还没有结束,则会阻塞线程,直到任务执行完毕。可以设置等待时间,超出时间或者返回结果为null,则抛出异常cancel():
//自定义CallAble并实现call()方法 class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { int a = 1; for (int i = 0; i <100000; i++) { a++; } Thread.sleep(2000); return a; } } public static void main(String[] args) { //新建CallAble MyCallable callable = new MyCallable(); //新建FutureTask任务 FutureTask<Integer> futureTask = new FutureTask<Integer>(callable); //新建Thread并与FutureTask关联 Thread thread = new Thread(futureTask, "A"); thread.start(); int sum; try { //拿到callable的结果 sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
线程池
线程池的创建方式有两种,
手动创建线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
要注意线程池的几个参数:
- corePoolSize :核心线程数
- maximumPoolSize :允许创建的最大线程数
- keepAliveTime :这是空闲线程保持存活的最大时间。
- unit : keepAliveTime的时间单位
- workQueue :任务队列,保存等待执行的任务的阻塞队列
- threadFactory :用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有含义的名字
- handler :饱和策略,当队列和线程池都满了,说明线程处于饱和状态,必须采取一种策略处理提交的新任务。默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
向线程池提交任务execute()和submit():
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>()); //使用execute提交任务 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("我是线程池"); } }); //之前说的callable MyCallable callable = new MyCallable(); //使用submit提交任务 返回一个future Future<Integer> future = threadPoolExecutor.submit(callable); try { //通过future拿到执行callable的结果 int x = future.get(); System.out.println(x); } catch (InterruptedException e) { //中断异常 e.printStackTrace(); } catch (ExecutionException e) { //无法执行任务异常 e.printStackTrace(); }finally { //关闭线程池 threadPoolExecutor.shutdown(); }
使用自定义线程池要合理的配置各个参数。
- 根据任务的性质:CPU密集型还是IO密集型设置核心线程数的大小,cpu密集型应配置尽可能小的线程,可以配置为 CPU个数+1。由于IO密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,可以配置为 2*cpu个数
- 优先级不同的任务,可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行
- 建议使用有界队列,有界队列能增加系统的稳定性和预警能力。
使用Executors创建线程池
这种创建方式本质上也是对ThreadPoolExecutor的参数进行不同的配置,每条创建后边把源码中不同的配置放到后边,以便更清晰的展示出来
//会根据需要创建新线程的线程池 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //核心线程数设置为0,最大线程数则是无界,的也就是说某些情况下会有无限多的线程被创建,从而导致一些问题,空闲线程最大等待时长是60秒 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } //使用单个worker线程 ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); //核心线程数和最大线程数都设置为1,并使用无界队列作为线程池的工作队列,其容量为无限大,某些情况下会导致队列中有太多的任务而出错 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } //可重用固定线程数的线程池 ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10); //核心线程数和最大线程数都设置为创建时传入的数量,空闲等待时长为0,说明空闲线程会立即终止,而任务队列容量也是无限大,可能会导致同样的问题 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } //可以用来在给定延时后执行异步任务或者周期性执行任务 ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(2); //核心线程的个数为指定的数量,允许的最大线程数是无限的 public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler); }
Android中特有的实现多线程
使用HandlerThread
HandleThread本质上就是一个线程,其继承了Thread。它的内部有自己的looper对象,并在run方法中通过Looper.prepare()来创建消息队列,并通过Looper.loop()开启消息循环,这样在使用中就允许在HandlerThread中创建Handler了
HandlerThread和普通的Thread是有显著的区别的,普通的Thread主要在run方法中执行一个耗时任务,而HandlerThread在内部创建了消息队列,外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务,由于handlerThread的run方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过其quit或者quitSafely方法来终止线程的执行。
public class HandlerThread extends Thread { Looper mLooper; private @Nullable Handler mHandler; @Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; } ... }
具体使用:
//创建handlerThread,标记一个名字 HandlerThread handlerThread = new HandlerThread("MyHandlerThread"); //启动线程 handlerThread.start(); //创建工作线程的handler,关联handlerThread的Looper对象。 //复写handleMessage处理消息 Handler myHandler = new Handler(handlerThread.getLooper()) { @Override public void handleMessage(@NonNull Message msg) { Log.i(msg.what + msg.obj.toString()); super.handleMessage(msg); } }; //定义消息 Message msg = new Message(); msg.what = 1; msg.obj = "A"; //发送消息到其绑定的消息队列中 myHandler.sendMessage(msg); //结束线程,停止线程的消息循环 handlerThread.quit();
使用IntentService
本质上是一个Service,可以看做是Service和HandlerThread的结合体,它继承了Service并且是一个抽象类,因此需要创建它的子类才能使用IntentService。可以用于执行后台耗时任务,由于是服务,所以它的优先级比单纯的线程要高很多,并且不容易被系统杀死。有如下几个特点
- IntentService是继承自service并处理异步请求的一个类,其内部有一个工作线程处理耗时请求
- 任务执行完毕,会自动销毁
- 如果IntentService启动多次,每个耗时操作会以队列的方式在IntentService的onHandleIntent方法中依次执行,串行执行结束后,会自动销毁。
首先看一下IntentService的源码:
//继承自service public abstract class IntentService extends Service { private volatile Looper mServiceLooper; @UnsupportedAppUsage private volatile ServiceHandler mServiceHandler; private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { //处理消息时调用onHandleIntent方法 我们需要重写这个方法实现自己的处理逻辑 onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); } } ... @Override public void onCreate() { super.onCreate(); //创建handlerThread并启动 HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); mServiceLooper = thread.getLooper(); //初始化handler并绑定handlerThread的looper mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; //通过handler发送消息给HandlerThread处理 mServiceHandler.sendMessage(msg); } ...
当IntentService被启动时,它的onCreate方法被调用,onCreate方法会创建一个HandlerThread,然后使用它的Looper来构造一个Handler对象,这样通过Handler发送的消息最终会在HandlerThread中执行。每次启动IntentService,它的onStartCommand方法就会调用一次,IntentService在onStartCommand中处理每个后台任务的Intnet。onStartCommond调用了onStart,在onStart方法中,IntentService通过handler发送一个消息,这个消息会在HandlerThread中处理。当onHandleIntent方法执行结束后,IntentService会通过stopSelf方法尝试停止服务,onHandlerIntent是一个抽象方法,需要我们在子类中实现,它的作用是从Intent参数中区分具体的任务并执行这些任务。
如何使用?
- 新建service类并继承自IntentService
- 实现service的构造方法
- 在manifast.xml中注册服务
- 在服务的onHandleIntent方法中实现业务逻辑
自定义一个IntentService
public class MyIntentService extends IntentService { public MyIntentService(String name) { super(name); } @Override protected void onHandleIntent(@Nullable Intent intent) { if(intent != null){ String action = intent.getAction(); if(action.equals("A")){ doSomeThingA(); }else if(action.equals("B")){ doSomeThingB(); } } } }
JobIntentService/JobScheduler
系统不允许后台应用创建后台服务,因此8.0引入了Context.startForegroundService(),以在前台启动新服务,
由于Android O的后台限制,创建后台服务需要使用JobScheduler来由系统进行调度任务的执行,而使用JobScheduler的方式比较繁琐,8.0及以上提供了JobIntentService帮助开发者更方便的将任务交给JobScheduler调度,其本质是Service后台任务在他的OnhandleWork()中进行,子类重写该方法即可。使用较简单。
public class SimpleJobIntentService extends JobIntentService { /** * 这个Service 唯一的id */ static final int JOB_ID = 1000; /** * 将工作加入此服务的方法,使用中调用这个方法 */ static void enqueueWork(Context context, Intent work) { enqueueWork(context, SimpleJobIntentService.class, JOB_ID, work); } //在这里执行耗时任务 @Override protected void onHandleWork(Intent intent) { Log.i("SimpleJobIntentService", "Executing work: " + intent); String label = intent.getStringExtra("label"); if (label == null) { label = intent.toString(); } toast("Executing: " + label); for (int i = 0; i < 5; i++) { Log.i("SimpleJobIntentService", "Running service " + (i + 1) + "/5 @ " + SystemClock.elapsedRealtime()); try { Thread.sleep(1000); } catch (InterruptedException e) { } } Log.i("SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime()); } @Override public void onDestroy() { super.onDestroy(); toast("All work complete"); } final Handler mHandler = new Handler(); // Helper for showing tests void toast(final CharSequence text) { mHandler.post(new Runnable() { @Override public void run() { Toast.makeText(SimpleJobIntentService.this, text, Toast.LENGTH_SHORT).show(); } }); } }
activity中通过调用enqueueWork()方法来启动
Intent workIntent = new Intent(); num++; Log.d("houson", "onClick: "+num); workIntent.putExtra("work","work num:"+num); //调用enqueueWork方法 MyJobIntentService.enqueueWork(getApplicationContext(),workIntent);
WorkManager
根据需求的不同,Android为后台任务提供了多种解决方案,如JobScheduler,Loader,Service等。如果这些API没有被适当地使用,可能会消耗大量的电量。WorkManager的出现,为应用程序中那些不需要及时完成的任务,提供统一的解决方案,以便在设备电量和用户体验之间达到一个比较好的平衡。
WorkManager最低可以兼容API Level 14,在API Level 23+,通过JobScheduler来完成任务,23一下的设备,通过AlarmManager和BroadCastReceivers组合完成任务。
WorkManager主要针对不需要及时完成的任务,如发送日志,同步应用程序数据,备份等。并且可以保证任务一定会被执行,即使应用当前不在运行中,WorkManager有自己的数据库,关于任务的所有信息和数据都保存在这个数据库中。因此只要你的任务交给了WorkManager,哪怕你的应用程序彻底退出,或者设备重新启动,WorkManager依然能够保证完成你交给的任务。
如何使用?
添加依赖
dependencies { def versions = "2.2.0" implementation "androidx.work:work-runtime:$versions" }
定义Worker任务:
worker是任务的执行者,是一个抽象类,需要继承它实现要执行的任务。
//继承worker,自定义worker class MyWork extends Worker{ public MyWork(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } //执行耗时任务 @NonNull @Override public Result doWork() { //拿到传进来的数据 String input_data = getInputData().getString("input_data"); // 任务执行完成后返回数据 Data outputData = new Data.Builder().putString("output_data", "Success!").build(); // 执行成功返回Result.success() // 执行失败返回Result.failure() // 需要重新执行返回Result.retry() return Result.success(outputData); } }
使用WorkRequest配置任务。
WorkRequest指定让哪个 Woker 执行任务,指定执行的环境,执行的顺序等。要使用它的子类 OneTimeWorkRequest(执行一次) 或 PeriodicWorkRequest(周期执行)。
通过WorkRequest配置我们的任务何时运行以及如何运行
//设置任务的触发条件 Constraints constraints = new Constraints.Builder() .setRequiresCharging(true) // 在充电时执行 .setRequiresStorageNotLow(true) // 不在存储容量不足时执行 // 网络状态 //NOT_REQUIRED--没有要求 //CONNECTED--网络连接 //UNMETERED--连接无限流量的网络 //METERED--连接按流量计费的网络 //NOT_ROAMING--连接非漫游网络 .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresDeviceIdle(true) // 在待机状态下执行,需要 API 23 .setRequiresBatteryNotLow(true)// 不在电量不足时执行 .build(); //然后将该Constraints设置到WorkRequest。WorkRequest是一个抽象类, //它有两种实现OneTimeWorkRequest和PeriodicWorkRequest,分别对应的是一次性任务和周期性任务。 //定义要传递的数据 Data inputData = new Data.Builder().putString("input_data", "Hello").build(); OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWork.class) .setConstraints(constraints) .setInputData(inputData) .build();
WorkManager
将任务提交给系统。管理任务请求和任务队列,发起的 WorkRequest 会进入它的任务队列。WorkManager.enqueue()方法会将你配置好的WorkRequest交给系统来执行。
WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);
观察任务的状态
任务在提交给系统后,通过WorkInfo获知任务的状态,WorkInfo包含了任务的id,tag,以及Worker对象传递过来的outputData,以及任务当前的状态。有三种方式可以得到WorkInfo对象。
通过LiveData,我们便可以在任务状态发生变化的时候,收到通知。同时通过LiveData的WorkInfo.getOutputData(),得到从Worker传递过来的数据。
WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneTimeWorkRequest.getId()).observe( MainActivity.this, new Observer<WorkInfo>() { @Override public void onChanged(WorkInfo workInfo) { Log.d("onChanged()->", "workInfo:"+workInfo); if (workInfo != null && workInfo.getState() == WorkInfo.State.SUCCEEDED) { String outputData = workInfo.getOutputData().getString("output_data"); Log.d("onChanged()->", "outputData:"+outputData); } } } );
取消任务
WorkManager.getInstance(MainActivity.this).cancelAllWork();
以上就是WorkManager的一些基本使用,更详细的可以参照官方文档或者其他文章,这里就不仔细讲解了。工作中,我们经常需要处理后台任务,如果处理后台任务所采用的API没有被正确使用,那么很可能会消耗大量设备的电量。Android出于设备电量的考虑,为开发者提供了WorkManager,旨在将一些不需要及时完成的任务交给它来完成。
使用协程
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码,协程是轻量级的线程,为什么是轻量的?因为它基于线程池API。协程可以使用阻塞式的方式写出非阻塞式的代码,解决回调地狱的问题。
协程有如下的特点:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
引入协程
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
使用
fun main() = runBlocking { launch { delay(1000L) println("World!") } println("Hello") }
协程的使用当然不是这么简单,有很多的API,这里限于篇幅,就不深入讲解了。
AsyncTask
是一个轻量级的异步任务类,在高版本中已经过时了。但研究一下还是很有意义的。他可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新ui。AsyncTask封装了Thread和Handler,通过AsyncTask可以更加方便的执行后台任务以及在主线程中访问ui。
AsyncTask提供了四个核心方法:
- onPreExecute:在主线程中执行,在异步任务执行之前,此方法会被调用,一般用于一些准备工作
- doInBackground:在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数,此方法中可以通过publishProgress来更新任务的进度。
- onProgressUpdata(Progress...values):在主线程中执行,当后台任务的执行进度发生改变时调用此方法
- onPostExecute:在主线程中执行,在异步任务执行之后,此方法会被调用,result参数是后台任务的返回值,即doInbackground的返回值
AsyncTask在使用过程中有一些限制条件:
- AsyncTask必须在主线程中加载,也就是第一次访问AsyncTask必须发生在主线程
- AsyncTask的对象须在主线程中创建
- execute方法必须在ui线程调用
- 不要在程序中直接调用onPreExecute、onPostExecute、doInBackground、onProgressUpdate方法
- 一个AsyncTask对象只能执行一次,也就是只能调用一次execute方法,否则会报异常
原理分析:
从execute方法开始分析,调用executeOnExecutor方法,sDefaultExecutor是一个串行的线程池如下:
@MainThread public final AsyncTask<Params, Progress, Result> execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); } @MainThread public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) { if (mStatus != Status.PENDING) { switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } } mStatus = Status.RUNNING; onPreExecute(); mWorker.mParams = params; //线程池开始执行 封装为Futuretask交个线程池处理 exec.execute(mFuture); return this; }
sDefaultExecutor是一个串行线程池,一个进程中所有的AsyncTask任务都在这个线程池中排队执行。首先系统会把AsyncTask的Params参数封装为FutureTask对象,这里FutureTask充当Runnable,接着这个FutureTask会交给SerialExecutor的execute方法处理,SerialExecute的execute方法首先把FutureTask对象插入到任务队列中,接着调用scheduleNext方法执行FutureTask任务,从队列中取出任务交给THREAD_POOL_EXECUTOR线程池去真正执行任务。InternalHandler用于将执行环境从线程池切换到主线程。
//串行线程池 private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); //线程池用于真正执行任务 public static final Executor THREAD_POOL_EXECUTOR; //用于将执行环境从线程池切换到主线程 private static InternalHandler sHandler; //初始化执行任务的线程池 static { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); threadPoolExecutor.allowCoreThreadTimeOut(true); THREAD_POOL_EXECUTOR = threadPoolExecutor; } //用于调度任务的串行线程池 private static class SerialExecutor implements Executor { final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { //从队列中取出任务交个THREAD_POOL_EXECUTOR线程池去真正执行任务 if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } }
再看一下任务future,call方法中调用postResult方法,通过handler发送消息,最终在handleMessage方法中进行处理。
private final WorkerRunnable<Params, Result> mWorker; private final FutureTask<Result> mFuture; mWorker = new WorkerRunnable<Params, Result>() { public Result call() throws Exception { mTaskInvoked.set(true); Result result = null; try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //noinspection unchecked result = doInBackground(mParams); Binder.flushPendingCommands(); } catch (Throwable tr) { mCancelled.set(true); throw tr; } finally { postResult(result); } return result; } }; mFuture = new FutureTask<Result>(mWorker) { @Override protected void done() { try { postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); } } };
结语
以上介绍了几种实现多线程操作的方式,有些并没有深入分析,当然了,实现多线程的方式不止以上的几种,还请各位看官多多补充,如有错误的地方还请指出。
到此这篇关于Android中实现多线程操作的几种方式的文章就介绍到这了,更多相关Android多线程操作 内容请搜索海外IDC网以前的文章或继续浏览下面的相关文章希望大家以后多多支持海外IDC网!
【原URL http://www.yidunidc.com/sin.html 转载请说明出处】