Android AsyncTask 原理分析

一、前言

如果通过 Thread 执行耗时操作,那么在操作完成之后,我们可能需要更新 UI,这时通常的做法就是通过 Handler 投递一个消息给 UI 线程,然后更新 UI。这种方式对于整个过程的控制比较精细,但是也有缺点,例如:代码相对臃肿,在多个任务同时执行时,不易对线程进行精确控制。

为了简化操作,Android 1.5 提供了工具类 AsnycTask,不再需要编写任务线程和 Handler 实例即可完成相同的任务。它更重量级,更易于使用。

二、AsyncTask 的定义与核心方法

public abstract class AsyncTask<Params, Progress, Result> { }

3 种泛型类型分别表示参数类型、后台任务执行的进度类型、返回的结果类型。不是每个参数都一定需要,如果不需要某个参数,可以设置为 Void。

一个异步任务的执行一般包括以下几个步骤:
  1. execute(Params... params):执行在 UI 线程。执行一个异步任务,在代码中调用此方法,触发异步任务的执行。

  2. onPreExecute():执行在 UI 线程,在 execute(Params... params) 被调用后立即执行,一般用来在执行后台任务前对 UI 做一些标记。

  3. doInBackground(Params... params):执行在子线程。在 onPreExecute() 完成后立即执行,用于执行较为耗时的操作,此方法将接收输入参数和返回计算结果。在执行过程中可以调用 publishProgress(Progress... values) 来更新进度信息。

  4. onProgressUpdate(Progress... values):执行在 UI 线程,在调用 publishProgress(Progress... values) 时,此方法被执行,直接将进度信息更新到 UI 组件上。

  5. onPostExecute(Result result):执行在 UI 线程。当后台操作结束时,此方法将会被调用,doInBackground 函数返回的计算结果将作为参数传递到此方法中,直接将结果显示到 UI 组件上。

在使用的时候,有几点需要注意:
  1. 异步任务的实例必须在 UI 线程中创建。

  2. execute(Params... params) 方法必须在 UI 线程中使用。

  3. 不能在 doInBackground(Params... params) 中更改 UI 组件的信息。

  4. 一个任务实例只能执行一次,如果执行第二次将会抛出异常。

三、AsyncTask 的实现基本原理

AsyncTask 内部是怎么执行的呢?它执行的过程与我们使用 Handler 又有什么区别呢?

我们先来看看 AsyncTask 里面的几个核心方法:

    //这是一个 abstract 方法,因此必须覆写
    @WorkerThread
    protected abstract Result doInBackground(Params... params);

    //执行在 doInBackground 之前,并且执行在 UI 线程
    @MainThread
    protected void onPreExecute() {
    }

    //后台操作执行完成后会调用的方法,在此更新 UI
    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onPostExecute(Result result) {
    }

    //在此更新进度,调用 publishProgress(Progress... values) 时,
    //此方法被执行
    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onProgressUpdate(Progress... values) {
    }


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


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


     * 执行任务,注意 execute 方法必须在 UI 线程中调用
     * @param exec 执行任务的线程池
     * @param params 参数
     * @return AsyncTask 实例
    @MainThread
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            //状态检测,只有在 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();

        //UI 线程传递过来的参数
        mWorker.mParams = params;

        //交给线程池管理器进行调度,参数为 FutureTask 类型,
        //构造 mFuture 时 mWorker 被传递了进去
        exec.execute(mFuture);

        //返回自身,使得调用者可以保持一个引用
        return this;
    }
  • publishProgress(Progress... values) 是 final 修饰的,不能覆写,只能去调用,一般会在 doInBackground(Params... params) 中调用此方法来更新进度条。

1. Status 枚举类

可以看到有一个 Status 的枚举类和 getStatus() 方法,Status 枚举代表了 AsyncTask 的状态,Status 枚举类代码如下:

    //初始状态,未执行状态
    private volatile Status mStatus = Status.PENDING;

    public enum Status {
        //未执行状态
        PENDING,

        //执行中
        RUNNING,
        
        //执行完成
        FINISHED,
    }

    //返回当前状态
    public final Status getStatus() {
        return mStatus;
    }

这几个状态在 AsyncTask 一次生命周期中很多地方被用到,非常重要。在调用 execute 时会判断该任务的状态,如果是非 PENDING 状态则会抛出异常,因此一个 AsyncTask 实例只能运行一次,因为运行过后,AsyncTask 的状态就变成 FINISHED 状态。

2. sDefaultExecutor

关于 sDefaultExecutor,它是 ThreadPoolExecute 的实例,用于管理提交到 AsyncTask 的任务,代码如下:

    //默认的任务调度是顺序执行
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    //顺序执行的 Executor
    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() {
            if ((mActive = mTasks.poll()) != null) {
                //将任务交给 THREAD_POOL_EXECUTOR 执行
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

上述代码可以看出,sDefaultExecutor 只负责将异步任务分发给THREAD_POOL_EXECUTOR 线程池,因此,真正执行任务的地方是 THREAD_POOL_EXECUTOR。

THREAD_POOL_EXECUTOR 的构造代码如下:

    //当前可用的 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;
    //线程空闲时的存留时间
    private static final int KEEP_ALIVE_SECONDS = 30;

    //线程工厂
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        //新建一个线程
        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    //线程队列
    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    //线程池
    public static final Executor THREAD_POOL_EXECUTOR;

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

更多有关线程池的介绍可以看这篇博文 Android 多线程探索(三)— 线程池

3. mWorker 与 mFuture

  • mWorker 实际上是 AsyncTask 的一个抽象内部类的实现对象实例,它实现了 Callable<Result> 接口中的 call() 方法。

  • mFuture 实际上是 FutureTask 的实例,FutureTask 是一个可管理的异步任务,使得这些异步任务可以被更精确的控制。

它们的初始化都在 AsyncTask 的构造函数中,代码如下:

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //执行 doInBackground
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    //调用 postResult  将结果投递给 UI 线程
                    postResult(result);
                }
                return result;
            }
        };

        //在 mFuture 实例中,将会调用 mWorker 做后台任务,完成后会调用 done 方法
        //这里将 mWorker 作为参数传递给了 mFuture 对象
        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    //如果 postResult 没有被执行,
                    //那么执行 postResultIfNotInvoked(),
                    //这里判断的是 mTaskInvoked 成员变量
                    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);
                }
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

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

mWorker 的 call 函数中调用了 doInBackground 函数,并且最后通过 postReult 函数将结果投递出去。如果 postResult 没有被调用,那么最终在 mFuture 的 done 函数就会调用 postResult 函数分发结果。

4. execute 函数

对于 execute 函数,我们来分析一下它的执行流程:

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


     * 执行任务,注意 execute 方法必须在 UI 线程中调用
     * @param exec 执行任务的线程池
     * @param params 参数
     * @return AsyncTask 实例
    @MainThread
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        ......
        mStatus = Status.RUNNING;

        //执行任务前的准备处理
        onPreExecute();

        //UI 线程传递过来的参数
        mWorker.mParams = params;

        //交给线程池管理器进行调度,参数为 FutureTask 类型,
        //构造 mFuture 时 mWorker 被传递了进去
        exec.execute(mFuture);

        //返回自身,使得调用者可以保持一个引用
        return this;
    }

exec.execute(mFuture) 最终会进入到 ThreadPoolExecutor 的 execute 函数,如下:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

忽略对工作线程、任务数量的判断,这段代码的主要功能是将异步任务 mFuture 加入将要执行的队列中,因此我们关注的函数是 addWorker():

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        ...
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //这里又生成了一个 Worker 对象,将异步任务传递给 w
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                        ...
                        //将 w 添加到 workers 里,这是一个 HashSet 集合对象
                        workers.add(w);
                        ...
                if (workerAdded) {
                    //启动该异步任务,即启动 mFuture 任务
                    t.start();
                    workerStarted = true;
                }
            }
        }
        ...
        return workerStarted;
    }

上面代码将 mFuture 实例传递到 Worker 中,并将该 Worker 对象添加到工作线程队列等待执行。mFuture 是 FutureTask 类型,构造的时候传递的参数是 mWorker:

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    public void run() {
        ...
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //这里调用的是 callable 的 call 函数,
                    //即 mWorker 的 call 函数
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    //代码省略
                }
                if (ran)
                    set(result);
            }
             //代码省略
    }

当启动 mFuture 时就会执行其中的 run 函数,实际上调用的是 callable(这里是 mWorker)的 call 函数,mWorker 的 call 函数又调用了 doInBackground 函数,此时线程真正启动了。获取到了 call 函数的结果之后,调用了 set(result) 函数:

    protected void set(V v) {
            ...
            finishCompletion();
        }
    }

    private void finishCompletion() {
        ...
        done();
        callable = null;        // to reduce footprint
    }

可以看到,set(result) 方法中又调用了 done() 方法。如果任务顺利执行完成,就会执行 done 方法。我们之前覆写了 mFuture 的 done 方法,获取调用结果,然后通过 postResult 将结果投递给 UI 线程。

5. sHandler

sHandler 实际上是 AsyncTask 内部类 InternalHandler 的实例,InternalHandler 继承了 Handler,下面我们来分析一下它的代码:

    private static InternalHandler sHandler;
    //mHandler 是 sHandler 的引用,
    //详细请看 AsyncTask 的构造方法
    private final Handler mHandler;

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    private static Handler getMainHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler(Looper.getMainLooper());
            }
            return sHandler;
        }
    }

    private Handler getHandler() {
        return mHandler;
    }

    private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

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

在处理消息时,遇到 MESSAGE_POST_RESULT,它会调用 AsyncTask.finish 方法,我们来看下它的定义:

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            //调用 onPostExecute 显示结果
            onPostExecute(result);
        }
        //改变状态为 FINISHED
        mStatus = Status.FINISHED;
    }

原来,finish() 方法是负责调用 onPostExecute(Result result) 方法并改变任务状态。onPostExecute 函数执行在 sHandler 的线程中,sHandler 关联的就是 UI 线程,因此 onPostExecute 函数就执行在 UI 线程上了。

如果被开发者覆写的 doInBackground(Params... params) 方法中调用了 publishProgress(Progress... values) 方法,则通过 InternalHandler 实例 sHandler 发送一条 MESSAGE_POST_PROGRESS 消息,更新进度,sHandler 处理消息时,onProgressUpdate(Progress... values) 方法将被调用。

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

如果任务执行时遇到异常,mCancelled 会被设置为 false,这时 onCancelled(result) 函数会被调用:

    @SuppressWarnings({"UnusedParameters"})
    @MainThread
    protected void onCancelled(Result result) {
        onCancelled();
    }    
    
    @MainThread
    protected void onCancelled() {
    }

可以看到,onCancelled() 的方法体为空,我们可以对它进行覆写。

6. AsyncTaskResult

postResult 方法构建消息时,包含了一个 AsyncTaskResult 类型的对象。

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

然后在 sHandler 实例对象的 handleMessage(Message msg) 中获取该对象。

AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;

AsyncTaskResult 是 AsyncTask 的一个内部类,用来包装执行结果的一个类,我们来看看 AsyncTaskResult 的内部结构:

    @SuppressWarnings({"RawUseOfParameterizedType"})
    private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }

可以看到,AsyncTaskResult 封装了一个 AsyncTask 的实例和某种类型的数据集。

四、缺点:

1. 内存泄露问题

在 Activity 中使用非静态匿名内部 AsyncTask 类,由于 Java 内部类的特点,AsyncTask 内部类会持有外部类的隐式引用。由于 AsyncTask 的生命周期可能比 Activity 长,当 Activity 进行销毁 AsyncTask 还在执行时,由于 AsyncTask 持有 Activity 的引用,导致 Activity 对象无法回收,进而产生内存泄露。

2. 结果丢失

另一个问题就是因为屏幕旋转等原因造成 Activity 重新创建时 AsyncTask 数据丢失的问题。当 Activity 销毁并重新创建后,还在运行的 AsyncTask 会持有一个 Activity 的非法引用即之前的 Activity 实例。导致 onPostExecute() 没有任何作用。

3. 串行并行多版本不一致

1.6 之前为串行,1.6 到 2.3 为并行,3.0 之后又改回为串行,但是可以通过 executeOnExecutor(Executor) 实现并行处理任务。

五、总结

  • 当我们调用 execute(Params... params) 方法后,execute 方法会调用 onPreExecute() 方法,然后由 ThreadPoolExecutor 实例 THREAD_POOL_EXECUTOR 执行一个 FutureTask 任务,这个过程中 doInBackground(Params... params) 将被调用。

  • 如果被开发者覆写的 doInBackground(Params... params) 方法中调用了 publishProgress(Progress... values) 方法,则通过 InternalHandler 实例 sHandler 发送一条 MESSAGE_POST_PROGRESS 消息,更新进度,sHandler 处理消息时,onProgressUpdate(Progress... values) 方法将被调用;

  • 如果遇到异常,sHandler 处理消息时,onCancelled() 将会被调用,onCancelled() 是一个空方法;

  • 如果执行成功,sHandler 处理消息时会调用 onPostExecute(Result result) 方法让用户在 UI 线程处理结果。

这里有个简单示例 Android AsyncTask 简单示例

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

推荐阅读更多精彩内容