Architecture(1)AsyncTask源码分析

概述

从事Android开发以来,研究过很多编程方面的东西,有编程基础:网络编程,数据结构跟算法,Java知识点:Java基础,JVM,并发编程,Android知识点:Android基础,Binder机制,性能优化等。这些都是一些具体的知识点,很零散,总想着把这些知识点串起来,不然很容易忘记,所以就打算开始研究一些框架,之前也零零散散看过一些框架,但没有总结,自己也封装过一些框架,所以打算系统的学习一些关于架构方面的知识,不再仅仅地从原理上知道为什么要这么写,想从细节上来分析一下各个框架,打算重点研究一下AsyncTaskVolleyPicassoLitePal。这些是我刚刚从事Android开发的时候接触到的几个框架,虽然后来也用过OKHttp,Rxjava,Retrofit,Glide等,但是最终还是对这几个框架进行分析分析,毕竟原理都是一样的。

正文

注释

AsyncTask enables proper and easy use of the UI thread. This class allows you
 to perform background operations and publish results on the UI thread without
 having to manipulate threads and/or handlers.

AsyncTask让对UI线程的操作变得简单。这个类可以让你在不需要控制多线程或者Handlers的情况下,可以在子线程中进行耗时操作并且将操作结果切换到UI线程

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
and does not constitute a generic threading framework. AsyncTasks should ideally be
used for short operations (a few seconds at the most.) If you need to keep threads
running for long periods of time, it is highly recommended you use the various APIs
provided by the <code>java.util.concurrent</code> package such as {@link Executor},
{@link ThreadPoolExecutor} and {@link FutureTask}.

AsyncTask被设计成为Thread跟Handler的帮助类并没有产恒一个线程框架。AsyncTasks在理想情况是用于较短的耗时操作(最多几秒钟)。如果你需要让线程持续运行很长一段时间,那么强烈推荐你使用Java的concurrent包下类似Executor,ThreadPoolExecutor,FutureTask这些API。

An asynchronous task is defined by a computation that runs on a background thread and
whose result is published on the UI thread. An asynchronous task is defined by 3 generic
types, called Params, Progress and Result,and 4 steps, called onPreExecute, doInBackground,onProgressUpdate and onPostExecute

异步的任务是这么被定义的:在子线程中进行计算,然后把计算结果显示在UI线程中。异步任务是通过Params, Progress and Result三个泛型参数,onPreExecute, doInBackground,onProgressUpdate and onPostExecute四个步骤来实现的

AsyncTask must be subclassed to be used. The subclass will override at least
 one method ({@link #doInBackground}), and most often will override a second one ({@link #onPostExecute}.)

异步任务必须在在实现类中进行使用,子类至少需要复写doInBackground方法,通常也需要复写onPostExecute方法

示例用法

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) 
               break;
         }
         return totalSize;
     }
 
     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }
 
     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }
//进行调用
new DownloadFilesTask().execute(url1, url2, url3);

泛型参数

Params, the type of the parameters sent to the task uponexecution.

参数,传递给即将执行任务的参数

Progress ,the type of the progress units published during the background computation.

进度,在子线程中进行算回传给UI线程的进度单元

Result, the type of the result of the background computation.

执行结果,后台计算的结果

执行步骤

onPreExecute(), invoked on the UI thread before the task is executed. This step is normally used to setup the task, for instance by showing a progress bar in the user interface.

onPreExecute,任务执行前在主线程中调用。这个步骤通常被用来对异步任务进行设置,例如可以在用户交互界面展示一个进度条。

doInBackground, invoked on the background thread immediately after {@ #onPreExecute()} finishes executing. This step is used to perform background computation that can take a long time. The parameters of the asynchronous task are passed to this step. The result of the computation must
 be returned by this step and will be passed back to the last step. This step can also use {@link #publishProgress} to publish one or more units of progress. These values are published on the UI thread, in the in the onProgressUpdate step。

doInBackground,在子线程中被调,当onPreExecute执行完成后会立即被触发。这个方法被用来执行一些耗时的计算。异步任务执行需要的参数通过此方法进行传递。后台任务执行的结果会通过此步骤进行返回,在这个方法中也可以进行调用publishProgress来实时显示任务执行的百分比。publishProgress是在主线程中进行调用。

onProgressUpdate, invoked on the UI thread after a call to {@link #publishProgress}. The timing of the execution is undefined. This method is used to display any form of progress in the user
interface while the background computation is still executing. For instance,it can be used to animate a progress bar or show logs in a text field

onProgressUpdate,在UI线程中被调用,当在doInBackground中进行调用publishProgress的时候会走此方法,这个方法的执行时间是不确定的。当后台任务仍然在执行的时候,此方法被用来在用户交互界面展示各种形式的进度条。例如,他可以被用来展示一个进度条或者在文本框里面显示log。

onPostExecute, invoked on the UI thread after the background computation finishes. The result of the background computation is passed to this step as a parameter

onPostExecute,在UI线程中调用,用来展示异步任务的执行结果,异步任务的后台执行结果通过一个参数传递给这个方法。

Cancelling a task

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking this method will cause subsequent calls to {@link #isCancelled()} to return true.After invoking this method, {@link #onCancelled(Object)}, instead of onPostExecute will be invoked after {@link #doInBackground(Object[])}returns. To ensure that a task is cancelled as quickly as possible, you should always check the return value of {@link #isCancelled()} periodically from
{@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

异步任务可以在任何时间通过cancel来被取消。调用此方法回使得isCancelled返回true.当调用完此方法之后,当异步任务执行之后就不会再调用onPostExecute方法而是调用onCancelled方法。为了保证异步任务尽可能快地被终止,如果可能的话,需要周期性的每次在doInBackground中进行检车isCanclled的返回值。

Threading rules

The AsyncTask class must be loaded on the UI thread. This is done automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
The task instance must be created on the UI thread.
Execute must be invoked on the UI thread.
Do not call onPreExecute(),  onPostExecute,doInBackground, onProgressUpdate manually.
The task can be executed only once (an exception will be thrown if a second execution is attempted.)

异步任务必须在主线程中进行调用(3.0之后,系统会帮我自动处理线程切换问题)

异步任务实例必须在主线程中进行创建

Execute方法必须在主线程中被调用

不要在主线程中手动调用onPreExecute(), onPostExecute,doInBackground, onProgressUpdate方法

异步任务只能被执行一次(如果被调用第二次将会抛出exception)

Memory observability

 AsyncTask guarantees that all callback calls are synchronized in such a way that the following
 operations are safe without explicit synchronizations.
 Set member fields in the constructor or onPreExecute, and refer to them in link doInBackground.
 Set member fields in doInBackground, and refer to them in onProgressUpdate and link          onPostExecute.

异步任务保证所有的回调都是同步的,这样一来下面的操作在不加锁的情况下都是线程安全的

在构造方法中跟onPreExecute设置成员变量,他们将会传递到doInBackground中

在doInBackground中设置参数,他们将会传递给onProgressUpdate跟onPostExecute

Order of execution

When first introduced, AsyncTasks were executed serially on a single background thread. Starting with DONUT, this was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting with HONEYCOMB, tasks are executed on a single thread to avoid common application errors caused by parallel execution.If you truly want parallel execution, you can invoke executeOnExecutor(Executor, Object[]) with THREAD_POOL_EXECUTOR

当刚刚引进AsyncTask的时候,他是在单线程中串行执行的。从1.6开始,加入了线程池允许多任务并行执行。在3.0之后,异步任务又被放入了单线程中来避免并发执行中造成的应用异常。如果你确实想并发执行,那么通过调用executeOnExecutor(Executor, Object[])方法,通过线程池来执行。

成员变量

    //CPU数量
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    //核心线程数
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    //最大线程数
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    //线程没有任务执行时最多保持30s会终止
    private static final int KEEP_ALIVE_SECONDS = 30;
    //线程是缓存队列,默认容量为128
    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);
    
    //线程池工厂
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
     /内置线程计数器
    private final AtomicInteger mCount = new AtomicInteger(1);
      public Thread newThread(Runnable r) {
      //创建线程后计数器加1
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };
    //串行线程池
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
    //并行线程池
    public static final Executor THREAD_POOL_EXECUTOR;
    //默认的线程池,采用的是串行的线程池
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    //任务执行完发送的消息标志
    private static final int MESSAGE_POST_RESULT = 0x1;
    //任务执行过程中更新任务进度的消息标志
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    //内部持有的Handler,用来切换线程
    private static InternalHandler sHandler;
    //实现了Callable接口的worker,可以拿到返回值的Runnable
    private final WorkerRunnable<Params, Result> mWorker;
    //FutureTask,实现了RunnableFuture接口
    private final FutureTask<Result> mFuture;
    //当前任务的状态,采用volatile关键字修饰,保持内存可见性,默认为待执行状态
    private volatile Status mStatus = Status.PENDING;
    //任务是否取消的标志,用AtomicBoolean保持可见性
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    //任务是否开始的标志,用AtomicBoolean保持可见性
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

这些成员变量都比较好理解,不过需要注意的是在成员变量中有三个线程池,一个是串行的线程池,一个是并行的线程池,还有一个是线程池的引用。串行线程池已经初始化,但是并行的线程池在变量的声明中并没有初始化,他的初始化是在一个静态代码块中

 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;
    }

构造方法

 public AsyncTask() {
        //创建一个Callable对象,将AsyncTask的实现类中的泛型Params,Result传递进去
        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;
            }
        };
    //创建一个FutureTask,将worker传递进去
        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);
                }
            }
        };
    }

Execute相关

execute Runnable

@MainThread
public static void execute(Runnable runnable) {
    sDefaultExecutor.execute(runnable);
}

仅仅调用默认线程池,去执行一个Runnable

executeOnExecutor

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    if (mStatus != Status.PENDING) {
    //判断AsyncTask的状态
        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)");
        }
    }
    //更改AsyncTask的状态
    mStatus = Status.RUNNING;
    //调用执行前方法
    onPreExecute();
    //参数进行赋值
    mWorker.mParams = params;
    //将创建好的Future放入线程池
    exec.execute(mFuture);
    return this;
}

execute Params

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
  //间接调用executeOnExecutor方法
    return executeOnExecutor(sDefaultExecutor, params);
}

串行线程池

private static class SerialExecutor implements Executor {
    //采用ArrayDeque来存放即将要执行的任务
    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();
                }
            }
        });
      //如果mActive为空,从队列中取任务执行
        if (mActive == null) {
            scheduleNext();
        }
    }
    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
          //如果队列不为空,则将任务丢进线程池中进行执行
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

从SerialExecutor的源码进行分析可以知道,实际上他并不是真正意义上的线程池,只是用来存放加进来的任务而已,最终起作用的还是并行的线程池,此时的并行的线程池的执行的每一个任务都是在上一个任务执行完毕后才进行执行的,所以也就是所谓的串行执行。

AsyncTask表面上看起来有三种启动方式,实际上只有两种,一种是把AsyncTask当成线程池,直接执行一个Runnable,这个有些大材小用,还有一种就是通过传递参数,也是我们使用地最多的那种。如果我们没有传入线程池,那么AsyncTask采用自己默认的串行线程池,如果自定义了线程池,那么就会采用我们自己的线程池,当然我们也可以不用定义,因为在成员变量的分析过程中,已经有一个定义好的线程池THREAD_POOL_EXECUTOR,作为一个静态变量,是可以直接使用的。

流程梳理

onPreExecute

此方法最先在execute的时候最先调用,然后不管是串行还是并行的线程池,都开始执行任务

doInBackground

此方法在构造方法中执行下面代码的时候会被调用

result = doInBackground(mParams);

onProgressUpdated

当doInBackground中调用publishProgress的时候,会通过Handler发送消息

@WorkerThread
protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

在Handler中处理消息

private static class InternalHandler extends Handler {
    public InternalHandler() {
      //在主线程中创建handler
        super(Looper.getMainLooper());
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_PROGRESS:
               //调用onProgressUpdated方法
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

onPostExecute

当Callable中的任务执行完拿到返回结果的时候

mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        mTaskInvoked.set(true);
        Result result = null;
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //拿到返回结果
            result = doInBackground(mParams);
            Binder.flushPendingCommands();
        } catch (Throwable tr) {
            mCancelled.set(true);
            throw tr;
        } finally {
          //通知主线程任务执行完毕
            postResult(result);
        }
        return result;
    }
};

通过postResult,也就是通过Handler切换线程

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

Handler处理消息

@Override
public void handleMessage(Message msg) {
    AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
    switch (msg.what) {
        case MESSAGE_POST_RESULT:
            // There is only one result
            //调用finish方法
            result.mTask.finish(result.mData[0]);
            break;
        case MESSAGE_POST_PROGRESS:
            result.mTask.onProgressUpdate(result.mData);
            break;
    }
}

finish方法

private void finish(Result result) {
    if (isCancelled()) {
    //如果任务被取消,调用onCancelled方法
        onCancelled(result);
    } else {
    //如果任务没有被取消,就调用onPostExecute方法
        onPostExecute(result);
    }
    //更新AsyncTask任务的状态
    mStatus = Status.FINISHED;
}

cancel方法

public final boolean cancel(boolean mayInterruptIfRunning) {
  //更改AsyncTask的状态
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

cancel可以传递一个布尔值mayInterruptIfRunning,其实很好理解,就是如果任务正在进行是否中断,然后最后调用的是mFuture的cancel方法,很好理解

总结

之前一直是在网上看别人的博客介绍AsyncTask,这次从源码的注释开始,系统的分析了一下他的源码,其实还是很简单的,关于一些注意事项跟各版本之间的差异源码的注释中已经很详细了,网上的分析也都是基于源码的注释而来,整体来讲,AsyncTask算是一个简单的网络请求框架,内部采用线程池,Handler,以及Callable实现,比较轻量级,适用于耗时不太长的异步操作,没有设计到缓存,定向取消请求等复杂的操作。使用起来比较简单,源码也很容易读懂,不过在使用的时候需要注意几点

内存泄漏

由于AsyncTask是Activity的内部类,所以会持有外部的一个引用,如果Activity已经退出,但是AsyncTask还没有执行完毕,那么Activity就无法释放导致内存泄漏。对于这个问题我们可以把AsyncTask定义为静态内部类并且采用弱引用,同时还可以在Activity的destroy方法中调用cancel方法来终止AsyncTask。

版本差异

在1.6~3.0的版本中,AsyncTask默认的线程池是并行执行的,1.6之前以及3.0之后的版本都是串行执行的,但是可以通过setDefaultExecutor传入自定义的线程池,依然可以是的线程并发执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 3,083评论 1 15
  • Android开发者:你真的会用AsyncTask吗? 导读.1 在Android应用开发中,我们需要时刻注意保证...
    cxm11阅读 2,659评论 0 29
  • 在Android中我们可以通过Thread+Handler实现多线程通信,一种经典的使用场景是:在新线程中进行耗时...
    吕侯爷阅读 1,978评论 2 23
  • AsyncTask AsyncTask是android再API-3中加入的类,为了方便开发人员执行后台操作并在UI...
    Mr韶先生阅读 298评论 0 1
  • #本文参加‘青春’大赛,本人保证本文为本人原创,如有问题则与主办方无关,自愿放弃评优评奖资格 平顶山学院 文学院 ...
    LiuBrOn阅读 422评论 2 50