导语:之前做习惯了Framework层的开发,今天在武汉斗鱼公司面试APP客户端的开发,其中一道题是讲述Asynctask的优缺点,我靠,我只是知道有这么一个东西,会用而已,看来之前的生活太过于安逸,这么基础的东西都不会,特意回家总结下。

转载请注明原文链接:http://www.cnblogs.com/yanyojun/p/6414919.html

1、Asynctask简介

1.1 使用方法简介

Asynctask作为Android的基础之一,怎么使用就不多讲解了,网上到处都是教程,建议查看Android官方API文档:https://developer.android.google.cn/reference/android/os/AsyncTask.html

这里只实现一个小Demo程序,供大家赏玩:

界面:

这个程序其实特别简单,就是两个按钮,点击分别用来测试AysncTask和Handler两种模式的实现,点击后会有相应的Log提示。

功能简介:

Asynctask的实现:

  1. private class IAsyncTask extends AsyncTask<String, Integer, String> {
  2. protected String doInBackground(String... args1) {
  3. Log.i(TAG, "doInBackground in:" + args1[0]);
  4. int times = 10;
  5. for (int i = 0; i < times; i++) {
  6. publishProgress(i);//提交之后,会执行onProcessUpdate方法
  7. }
  8. Log.i(TAG, "doInBackground out");
  9. return "over";
  10. }
  11.  
  12. /**
  13. * 在调用cancel方法后会执行到这里
  14. */
  15. protected void onCancelled() {
  16. Log.i(TAG, "onCancelled");
  17. }
  18.  
  19. /**
  20. * 在doInbackground之后执行
  21. */
  22. protected void onPostExecute(String args3) {
  23. Log.i(TAG, "onPostExecute:" + args3);
  24. }
  25.  
  26. /**
  27. * 在doInBackground之前执行
  28. */
  29. @Override
  30. protected void onPreExecute() {
  31. Log.i(TAG, "onPreExecute");
  32. }
  33.  
  34. /**
  35. * 特别赞一下这个多次参数的方法,特别方便
  36. * @param args2
  37. */
  38. @Override
  39. protected void onProgressUpdate(Integer... args2) {
  40. Log.i(TAG, "onProgressUpdate:" + args2[0]);
  41. }
  42. }

点击第一个按钮后会执行这里,点击按钮的写法如下:

  1. mBtnSyncTask.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. new IAsyncTask().execute("yanlog test");
  5. }
  6. });

执行结果的Log如下:

  1. 02-19 21:55:12.179 10824-11010/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in:yanlog test//doInbackground是在10824进程,11010线程中执行
  2. 02-19 21:55:12.179 10824-11010/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground out
  3. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:0//剩下的都是在10824线程中执行,Android特别好的是,主线程的线程号跟进程号是一致的
  4. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:1
  5. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:2
  6. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:3
  7. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:4
  8. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:5
  9. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:6
  10. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:7
  11. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:8
  12. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onProgressUpdate:9
  13. 02-19 21:55:12.184 10824-10824/com.plbear.asynctasktest I/AsyncTaskTest: onPostExecute:over

Handler+Message实现:

主要代码如下:

  1. private class IHandler extends Handler{
  2. @Override
  3. public void handleMessage(Message msg){
  4. switch(msg.what){
  5. case 1:
  6. Log.e(TAG,"handler:"+msg.obj);
  7. break;
  8. default:
  9. break;
  10. }
  11. }
  12. }

其中,调用地方如下:

  1. mBtnHandlerTest.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. final Handler handler = new IHandler();
  5. new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. for (int i = 0; i < 10; i++) {
  9. Message msg = new Message();
  10. msg.what = 1;
  11. msg.obj = new Integer(i);
  12. Log.e(TAG, "post message:" + i);
  13. handler.sendMessage(msg);
  14. }
  15. }
  16. }).start();
  17. }
  18. });

可以看到Log打印结果如下:

  1. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:0 //可以看到提交是在9319号子进程中提交
  2. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:1
  3. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:2
  4. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:3
  5. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:4
  6. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:5
  7. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:6
  8. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:7
  9. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:8
  10. 02-19 22:25:17.689 9234-9319/com.plbear.asynctasktest E/AsyncTaskTest: post message:9
  11. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:0 //可以看到提交完是在9234主线程中执行。
  12. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:1
  13. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:2
  14. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:3
  15. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:4
  16. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:5
  17. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:6
  18. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:7
  19. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:8
  20. 02-19 22:25:17.692 9234-9234/com.plbear.asynctasktest E/AsyncTaskTest: handler:9

以上,简单梳理了下怎么实现,不赘言。

1.2 Android 内部源码实现

关于Handler+Message+Message Queue+Looper的实现就不介绍了,老生常谈了。所以下面主要看一下AsyncTask的源码实现:

AsyncTask的核心方法应该是

  1. public final AsyncTask<Params, Progress, Result> execute(Params... params)

那我们就看下当调用了execute方法后,都发生了什么,下面是执行的序列图。

我知道我画的不够标准了,凑合着看吧。下面关于这个图的一些说明。

  • 在第4步,execute的时候,这个时候可以看到,doInBackground已经转到子线程中执行了,这个是很关键的一个点,我特意用了一个异步处理的箭头标注了。
  • 在第9步,当doInbackground执行完,执行到finish方法的时候,由通过sendMessage的方法回到了主线程中了,所以后面的onPostExecute和onCanceled都是在主线程中执行的。

嗯,就这么多吧。关于AsyncTask的源码我上传到github中了,大家对照着源码看会更清楚一点。

https://github.com/YanYoJun/AndroidSource/blob/master/AsyncTask.java

关于AsyncTask的源码分析,还有一篇博客写的很好,请参看:

http://www.infocool.net/kb/OtherMobile/201610/197431.html

备注:

源码里面还有三个地方值得深究下,分别是:

  • FutureTask值得看下,回头写了博客我把链接贴在这里
  • AsyncTask中的SerialExecutor类写的太漂亮了,回头单独写一个博客欣赏下。
  • 关于上面的ThreadPollExecutor我其实没有研究。。。回头写个博客研究下。

2、优点

简单,快捷

这个说法就是近乎于扯淡吧,主要还是看使用习惯,我就挺喜欢用Handler的。

但是Android定义了这个东西,可以看到各种消息封装的还是很不错的,很规范。大家可以按照这个“优美的框架”来写,代码不会太出格。

3、缺点

3.1  AsyncTask实际上后台线程之后一个!!!

今天仔细研究了下源码,发现网上写的大部分是错的,AsyncTask的真正的后台线程只有一个!!不信,看下面的代码:

我们首先定义一个IAsyncTAsk,其中的doInBackground方法这么写:

  1. private class IAsyncTask extends AsyncTask<String, Integer, String> {
  2. protected String doInBackground(String... args1) {
  3. /* Log.i(TAG, "doInBackground in:" + args1[0]);
  4. int times = 10;
  5. for (int i = 0; i < times; i++) {
  6. publishProgress(i);//提交之后,会执行onProcessUpdate方法
  7. }
  8. Log.i(TAG, "doInBackground out");*/
  9.  
  10. Log.i(TAG, "doInBackground in thread:" + args1[0]);
  11. try {
  12. int times = 4;
  13. for (int i = 0; i < times; i++) {
  14. Log.i(TAG, "thread alive:" + i + " for times"+args1[0]); //这个doInBackground就打印一个Log,然后sleep 20 毫秒
  15. Thread.sleep(20);
  16. }
  17. } catch (Exception e) {
  18.  
  19. }
  20. return "over";
  21. }

调用的地方这么写:

  1. int N = 5;
  2. for (int i = 0; i < N; i++) {
  3. Log.d(TAG,"asyncTask post Task:"+i);
  4. new IAsyncTask().execute("asyncTask times:"+i); //点击Button后,在onClick方法中建立5个后台子线程。
  5. }

我们来看打印结果:

  1. 02-20 21:48:08.206 14812-14812/com.plbear.asynctasktest D/AsyncTaskTest: asyncTask post Task:0 //在主线程中进行提交操作
  2. 02-20 21:48:08.211 14812-14812/com.plbear.asynctasktest D/AsyncTaskTest: asyncTask post Task:1
  3. 02-20 21:48:08.211 14812-14812/com.plbear.asynctasktest D/AsyncTaskTest: asyncTask post Task:2
  4. 02-20 21:48:08.211 14812-14812/com.plbear.asynctasktest D/AsyncTaskTest: asyncTask post Task:3
  5. 02-20 21:48:08.211 14812-14812/com.plbear.asynctasktest D/AsyncTaskTest: asyncTask post Task:4
  6. 02-20 21:48:08.212 14812-18067/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in thread:asyncTask times:0 //可以看到,虽然系统开起了18067、18068、
  7. 02-20 21:48:08.212 14812-18067/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:0 for timesasyncTask times:0//18069,18070这几个子线程,但是这几个子线程
  8. 02-20 21:48:08.232 14812-18067/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:1 for timesasyncTask times:0 //是串行执行的!!!震惊了有没有!!!
  9. 02-20 21:48:08.253 14812-18067/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:2 for timesasyncTask times:0 //这不是巧合,试了好几次都是这样!!
  10. 02-20 21:48:08.273 14812-18067/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:3 for timesasyncTask times:0
  11. 02-20 21:48:08.294 14812-18068/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in thread:asyncTask times:1
  12. 02-20 21:48:08.294 14812-18068/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:0 for timesasyncTask times:1
  13. 02-20 21:48:08.315 14812-18068/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:1 for timesasyncTask times:1
  14. 02-20 21:48:08.335 14812-18068/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:2 for timesasyncTask times:1
  15. 02-20 21:48:08.356 14812-18068/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:3 for timesasyncTask times:1
  16. 02-20 21:48:08.377 14812-18069/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in thread:asyncTask times:2
  17. 02-20 21:48:08.377 14812-18069/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:0 for timesasyncTask times:2
  18. 02-20 21:48:08.397 14812-18069/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:1 for timesasyncTask times:2
  19. 02-20 21:48:08.417 14812-18069/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:2 for timesasyncTask times:2
  20. 02-20 21:48:08.438 14812-18069/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:3 for timesasyncTask times:2
  21. 02-20 21:48:08.462 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in thread:asyncTask times:3
  22. 02-20 21:48:08.462 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:0 for timesasyncTask times:3
  23. 02-20 21:48:08.483 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:1 for timesasyncTask times:3
  24. 02-20 21:48:08.504 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:2 for timesasyncTask times:3
  25. 02-20 21:48:08.524 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:3 for timesasyncTask times:3
  26. 02-20 21:48:08.545 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: doInBackground in thread:asyncTask times:4
  27. 02-20 21:48:08.545 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:0 for timesasyncTask times:4
  28. 02-20 21:48:08.565 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:1 for timesasyncTask times:4
  29. 02-20 21:48:08.585 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:2 for timesasyncTask times:4
  30. 02-20 21:48:08.606 14812-18070/com.plbear.asynctasktest I/AsyncTaskTest: thread alive:3 for timesasyncTask times:4

你本来希望系统应该这么执行

但是实际上系统是这么执行的:

那么从源码看下为啥会这样吧。

AsyncTask中默认的Exector是这个private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;看下SERIAL_EXECUTOR是这么定义的

  1. public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); //这是一个串行处理的Executor
  2. .........................
  3. private static class SerialExecutor implements Executor {
  4. final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
  5. Runnable mActive;
  6.  
  7. public synchronized void execute(final Runnable r) {
  8. mTasks.offer(new Runnable() { //先把要执行的子线程统一丢到mTasks队列中,这其中封装一遍Runnable
  9. public void run() {
  10. try {
  11. r.run();
  12. } finally {
  13. scheduleNext(); //当前面一个子线程处理完,开始处理下一个
  14. }
  15. }
  16. });
  17. if (mActive == null) {
  18. scheduleNext();
  19. }
  20. }
  21.  
  22. protected synchronized void scheduleNext() { //依次从队列中取下一个元素,串行执行
  23. if ((mActive = mTasks.poll()) != null) {
  24. THREAD_POOL_EXECUTOR.execute(mActive);
  25. }
  26. }
  27. }

呵呵,这下子明白了吧。

Google为什么要怎么实现我不得而知,估计有什么我没有明白的好处在里面吧。那么有没有办法规避呢?

可以看到上面有一个THREAD_POOL_EXECUTOR,这个也是一个executor是这么定义的

  1. static {
  2. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
  3. CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
  4. sPoolWorkQueue, sThreadFactory);
  5. threadPoolExecutor.allowCoreThreadTimeOut(true);
  6. THREAD_POOL_EXECUTOR = threadPoolExecutor;
  7. }

可以看到这个是允许一定数量的子线程并行处理的。

其中参数是这么定义的

  1. private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
  2. // We want at least 2 threads and at most 4 threads in the core pool,
  3. // preferring to have 1 less than the CPU count to avoid saturating
  4. // the CPU with background work
  5. private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
  6. private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
  7. private static final int KEEP_ALIVE_SECONDS = 30;

按照一般理解,允许同时运行的CORE进程是4个,MAXIMUM_POOL_SIZE是17个。(注:这个数字是我用荣耀8手机跑出来的,其他手机可能会有不同)

而Android中的AsyncTask提供了一个方法:

  1. public static void setDefaultExecutor(Executor exec) {
  2. sDefaultExecutor = exec;
  3. }

所以规避方法是:通过这个方法可以设置默认的Exector,但是这个方法是hide的,也就是Google的隐藏方法,估计需要用一下反射来处理。我偷个懒,不去实现了。

4、总结

  本来嘛,我只是想简单写一下AsyncTask的一些相关知识,Copy一下网上的内容,但是没有想到写到最后,发现网上的大部分东西是错的,或者没有抓到重点。看来以后还是要自己亲自看代码,纸上得来终觉浅,便知此事要躬行。

  本文中用到的工程代码可以到我的github中查看,路径:https://github.com/YanYoJun/AsyncTaskTest

  好了,写完了,大家看到之后觉得还可以的话,点个赞哈。

Android Asynctask的优缺点的更多相关文章

  1. android Asynctask的优缺点?是否能同一时候并发100+asynctask呢?

    一  Asynctask的优缺点? AsyncTask,是android提供的轻量级的异步类,能够直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步运行的程度(能够通过接口实现UI ...

  2. 再谈Android AsyncTask的优缺点

    导语:之前做习惯了Framework层的开发,今天在武汉斗鱼公司面试APP客户端的开发,其中一道题是讲述Asynctask的优缺点,我靠,我只是知道有这么一个东西,会用而已,看来之前的生活太过于安逸, ...

  3. android AsyncTask介绍(转)

    android AsyncTask介绍 AsyncTask和Handler对比 1 ) AsyncTask实现的原理,和适用的优缺点 AsyncTask,是android提供的轻量级的异步类,可以直接 ...

  4. Android AsyncTask异步加载WebAPI

    之前做的程序一直存在很多问题,因为需要加载的Activity需要从网络加载数据.并没有完全正确的使用异步的方法去加载! 之前用的虽然是AsyncTask,但是在加载完成的时候还是并没有使用AsyncT ...

  5. Android -- AsyncTask 使用和缺陷

    一.AsyncTask的基本用法 由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它.在继承时我们可以为AsyncTask类指定三个泛型参数,这三个参数的用途如下: ...

  6. Android Asynctask与Handler的比较,优缺点区别,Asynctask源码

    1  AsyncTask实现的原理,和适用的优缺点 AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可以 ...

  7. android AsyncTask 的使用(转载)

    1 ) AsyncTask实现的原理,和适用的优缺点 AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可 ...

  8. android AsyncTask介绍 转载

    http://www.cnblogs.com/devinzhang/archive/2012/02/13/2350070.html AsyncTask和Handler对比 1 ) AsyncTask实 ...

  9. android AsyncTask介绍 AsyncTask和Handler对比

    1 ) AsyncTask实现的原理,和适用的优缺点 AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可 ...

随机推荐

  1. WINDOWS下搭建SVN服务器端的步骤分享(Subversion)

    1.获取svn程序 2.安装 Subversion(以下简称SVN)的服务器端和客户端.下载下来的服务器端是个 zip压缩包,直接解压缩即可,比如我解压到 E:\subversion .客户端安装文件 ...

  2. BZOJ 1257 [CQOI2007]余数之和sum ——Dirichlet积

    [题目分析] 卷积很好玩啊. [代码] #include <cstdio> #include <cstring> #include <cmath> #include ...

  3. NOIP前刷水行动

    2016.11.15 BZOJ1009:DP+矩阵乘法+KMP BZOJ1898:矩阵乘法 BZOJ4101:贪心,考虑我们往右边撞的时候,我们会向左边冲 ,于是枚举答案点利用一个指针计算即可! 20 ...

  4. 无锁同步-C++11之Atomic和CAS

    1.概要 本文是无锁同步系列文章的第一篇,主要探讨C++11中的Atomic. 我们知道在C++11中引入了mutex和方便优雅的lock_guard.但是有时候我们想要的是性能更高的无锁实现,下面我 ...

  5. Microsoft Visual 的变态

    Microsoft Visual 里面使用指针 的时候, 声明要放在函数开始的位置,否则报错,真变态啊 刚刚发现,C的变量必须在语块开始声明,后面声明会报错,太不灵活了

  6. mybatis sql中if判断传入Integer类型,传入0时,判断没有执行

    数据库中字段 isDisable 0:否:1:是 xml中判断 <if test="isDisable != null and isDisable !='' "> AN ...

  7. c#第5章 变量的更多内容 隐式和显式转换、枚举、结构、数组、

    1.目标数据 destination 英[ˌdestɪˈneɪʃn] 美[ˌdɛstəˈneʃən] n. 目的,目标; 目的地,终点; [罕用语] 预定,指定; 2.源数据 source 英[sɔ: ...

  8. Python计算一个项目中含有的代码行数

    最近想要知道以前做过的project有多少行代码,因为文件太多,直接手工数效率太低,于是编写一个python程序用来计算一个project有多少代码行. 首先,在一个项目中,有很多子文件夹,子文件夹中 ...

  9. iOS 之 WebView 简单使用

    1. 代理 UIWebViewDelegate 2. 创建 UIWebView myWebView=[[UIWebView alloc] initWithFrame:CGRectMake(, ,sel ...

  10. 后台邮箱配置SMTP函数,如何把发件人设置为自定义昵称

    默认的php的mail函数,太坑爹! 用PHPCMS配置好SMTP后,虽然能发送邮件. 但是收件人一项,显示的是邮箱地址@前面的内容. 今天研究了一下午,终于在一个论坛里找到了答案. 原来PHPCMS ...