`
xiaonao880516
  • 浏览: 58295 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Android多线程任务优化:实现后台预读线程

阅读更多
Android多线程任务优化:实现后台预读线程
分类: Android 2012-05-16 13:11 107人阅读 评论(0) 收藏 举报
上一篇博文我们可以知道,使用AsyncTask有导致应用FC的风险,而且AsyncTask并不能满足我们一些特定的需求。下面我们介绍一种通过模仿AsyncTask的封装方式,实现一个后台预读数据的线程。

描述:在空闲时对获取成本较高的数据(如要读取本地或网络资源)进行预读是提高性能的有效手段。为了给用户带来更好的交互体验,提高响应性,很多网络应用(如新闻阅读类应用)都在启动的时候进行预读,把网络数据缓存到sdcard或者内存中。

下面介绍一个实现预读的例子,打开应用之后会有一个欢迎界面,在打开欢迎界面的同时我们在后台启动预读线程,预读下一个Activity需要显示的数据,预读数据保存到一个静态的Hashmap中。

首先要编写我们的后台预读线程,遵循不重复造轮子的原则,我们对AsyncTask稍作修改就可以满足需求。下面是我们自定义的PreReadTask类代码:

PreReadTask.java 实现预读线程
Java代码 
package cn.caiwb; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.PriorityBlockingQueue; 
import java.util.concurrent.RejectedExecutionHandler; 
import java.util.concurrent.SynchronousQueue; 
import java.util.concurrent.ThreadPoolExecutor; 
import java.util.concurrent.TimeUnit; 
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.LinkedBlockingQueue; 
import java.util.concurrent.ThreadFactory; 
import java.util.concurrent.Callable; 
import java.util.concurrent.FutureTask; 
import java.util.concurrent.ExecutionException; 
import java.util.concurrent.TimeoutException; 
import java.util.concurrent.CancellationException; 
import java.util.concurrent.atomic.AtomicInteger; 
 
import android.content.SyncResult; 
import android.os.Process; 
import android.os.Handler; 
import android.os.Message; 
public abstract class PreReadTask<Params, Progress, Result> { 
    private static final String LOG_TAG = "FifoAsyncTask"; 
 
//    private static final int CORE_POOL_SIZE = 5; 
//    private static final int MAXIMUM_POOL_SIZE = 5; 
//    private static final int KEEP_ALIVE = 1; 
 
//    private static final BlockingQueue<Runnable> sWorkQueue = 
//            new LinkedBlockingQueue<Runnable>(); 
// 
    private static final ThreadFactory sThreadFactory = new ThreadFactory() { 
        private final AtomicInteger mCount = new AtomicInteger(1); 
 
        public Thread newThread(Runnable r) { 
            return new Thread(r, "PreReadTask #" + mCount.getAndIncrement()); 
        } 
    }; 
 
    private static final ExecutorService sExecutor = Executors.newSingleThreadExecutor(sThreadFactory);//只有一个工作线程的线程池 
 
    private static final int MESSAGE_POST_RESULT = 0x1; 
    private static final int MESSAGE_POST_PROGRESS = 0x2; 
    private static final int MESSAGE_POST_CANCEL = 0x3; 
 
    private static final InternalHandler sHandler = new InternalHandler(); 
 
    private final WorkerRunnable<Params, Result> mWorker; 
    private final FutureTask<Result> mFuture; 
 
    private volatile Status mStatus = Status.PENDING; 
 
    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */ 
    public enum Status { 
        /**
         * Indicates that the task has not been executed yet.
         */ 
        PENDING, 
        /**
         * Indicates that the task is running.
         */ 
        RUNNING, 
        /**
         * Indicates that {@link FifoAsyncTask#onPostExecute} has finished.
         */ 
        FINISHED, 
    } 
 
    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */ 
    public PreReadTask() { 
        mWorker = new WorkerRunnable<Params, Result>() { 
            public Result call() throws Exception { 
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 
                return doInBackground(mParams); 
            } 
        }; 
 
        mFuture = new FutureTask<Result>(mWorker) { 
            @Override 
            protected void done() { 
                Message message; 
                Result result = null; 
 
                try { 
                    result = get(); 
                } catch (InterruptedException e) { 
                    android.util.Log.w(LOG_TAG, e); 
                } catch (ExecutionException e) { 
                    throw new RuntimeException("An error occured while executing doInBackground()", 
                            e.getCause()); 
                } catch (CancellationException e) { 
                    message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, 
                            new PreReadTaskResult<Result>(PreReadTask.this, (Result[]) null)); 
                    message.sendToTarget(); 
                    return; 
                } catch (Throwable t) { 
                    throw new RuntimeException("An error occured while executing " 
                            + "doInBackground()", t); 
                } 
 
                message = sHandler.obtainMessage(MESSAGE_POST_RESULT, 
                        new PreReadTaskResult<Result>(PreReadTask.this, result)); 
                message.sendToTarget(); 
            } 
        }; 
    } 
 
    /**
     * Returns the current status of this task.
     *
     * @return The current status.
     */ 
    public final Status getStatus() { 
        return mStatus; 
    } 
 
    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute}
     * by the caller of this task.
     *
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     *
     * @param params The parameters of the task.
     *
     * @return A result, defined by the subclass of this task.
     *
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */ 
    protected abstract Result doInBackground(Params... params); 
 
    /**
     * Runs on the UI thread before {@link #doInBackground}.
     *
     * @see #onPostExecute
     * @see #doInBackground
     */ 
    protected void onPreExecute() { 
    } 
 
    /**
     * Runs on the UI thread after {@link #doInBackground}. The
     * specified result is the value returned by {@link #doInBackground}
     * or null if the task was cancelled or an exception occured.
     *
     * @param result The result of the operation computed by {@link #doInBackground}.
     *
     * @see #onPreExecute
     * @see #doInBackground
     */ 
    @SuppressWarnings({"UnusedDeclaration"}) 
    protected void onPostExecute(Result result) { 
    } 
 
    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     *
     * @param values The values indicating progress.
     *
     * @see #publishProgress
     * @see #doInBackground
     */ 
    @SuppressWarnings({"UnusedDeclaration"}) 
    protected void onProgressUpdate(Progress... values) { 
    } 
 
    /**
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked.
     *
     * @see #cancel(boolean)
     * @see #isCancelled()
     */ 
    protected void onCancelled() { 
    } 
 
    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally.
     *
     * @return <tt>true</tt> if task was cancelled before it completed
     *
     * @see #cancel(boolean)
     */ 
    public final boolean isCancelled() { 
        return mFuture.isCancelled(); 
    } 
 
    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when <tt>cancel</tt> is called,
     * this task should never run.  If the task has already started,
     * then the <tt>mayInterruptIfRunning</tt> parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     *
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *        task should be interrupted; otherwise, in-progress tasks are allowed
     *        to complete.
     *
     * @return <tt>false</tt> if the task could not be cancelled,
     *         typically because it has already completed normally;
     *         <tt>true</tt> otherwise
     *
     * @see #isCancelled()
     * @see #onCancelled()
     */ 
    public final boolean cancel(boolean mayInterruptIfRunning) { 
        return mFuture.cancel(mayInterruptIfRunning); 
    } 
 
    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return The computed result.
     *
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     */ 
    public final Result get() throws InterruptedException, ExecutionException { 
        return mFuture.get(); 
    } 
 
    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     *
     * @param timeout Time to wait before cancelling the operation.
     * @param unit The time unit for the timeout.
     *
     * @return The computed result.
     *
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     * @throws TimeoutException If the wait timed out.
     */ 
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException, 
            ExecutionException, TimeoutException { 
        return mFuture.get(timeout, unit); 
    } 
 
    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     *
     * This method must be invoked on the UI thread.
     *
     * @param params The parameters of the task.
     *
     * @return This instance of AsyncTask.
     *
     * @throws IllegalStateException If {@link #getStatus()} returns either
     *         {@link FifoAsyncTask.Status#RUNNING} or {@link FifoAsyncTask.Status#FINISHED}.
     */ 
    public final PreReadTask<Params, Progress, Result> execute(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; 
        sExecutor.execute(mFuture); 
 
        return this; 
    } 
 
    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of
     * {@link #onProgressUpdate} on the UI thread.
     *
     * @param values The progress values to update the UI with.
     *
     * @see #onProgressUpdate
     * @see #doInBackground
     */ 
    protected final void publishProgress(Progress... values) { 
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS, 
                new PreReadTaskResult<Progress>(this, values)).sendToTarget(); 
    } 
 
    private void finish(Result result) { 
        if (isCancelled()) result = null; 
        onPostExecute(result); 
        mStatus = Status.FINISHED; 
    } 
 
    private static class InternalHandler extends Handler { 
        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) 
        @Override 
        public void handleMessage(Message msg) { 
            PreReadTaskResult result = (PreReadTaskResult) msg.obj; 
            switch (msg.what) { 
                case MESSAGE_POST_RESULT: 
                    // There is only one result 
                    result.mTask.finish(result.mData[0]); 
                    break; 
                case MESSAGE_POST_PROGRESS: 
                    result.mTask.onProgressUpdate(result.mData); 
                    break; 
                case MESSAGE_POST_CANCEL: 
                    result.mTask.onCancelled(); 
                    break; 
            } 
        } 
    } 
 
    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> { 
        Params[] mParams; 
    } 
 
    @SuppressWarnings({"RawUseOfParameterizedType"}) 
    private static class PreReadTaskResult<Data> { 
        final PreReadTask mTask; 
        final Data[] mData; 
 
        PreReadTaskResult(PreReadTask task, Data... data) { 
            mTask = task; 
            mData = data; 
        } 
    } 



对比AsyncTask我们实际只修改了一个地方

Java代码 
private static final ExecutorService sExecutor = Executors.newSingleThreadExecutor(sThreadFactory);//只有一个工作线程的线程池 


通过Executors.newSingleThreadExecutor,我们把PreReadTask的的线程池设置成只有一个工作线程,并且带有一个无边界的缓冲队列,这一个工作线程以先进先出的顺序不断从缓冲队列中取出并执行任务。

创建完后台预读的线程。我们通过一个例子介绍如何使用这个后台预读线程。

这个例子由两个Activity组成,WelcomeActivity是欢迎界面,在欢迎界面中会停留三秒,在此时我们对数据进行预读,预读成功后保存到一个全局的静态hashmap中。MainActivity是主界面,在主界面中显示一个listview,listview中的图片是模拟从网络获取的,当静态hashmap中存在数据(也就是已经成功预读的数据)的时,从hashmap中取,如果不存在,才从网络获取。


WelcomeActivity.java 欢迎界面,停留三秒,预读数据
Java代码 
package cn.caiwb; 
import android.app.Activity; 
import android.content.Intent; 
import android.graphics.BitmapFactory; 
import android.os.AsyncTask; 
import android.os.Bundle; 
import android.os.Handler; 
import android.widget.Toast; 
 
public class WelcomeActivity extends Activity { 
     
    private Handler handler = new Handler(); 
     
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.welcome); 
        for(int i = 0; i < 15; i++) {//预读15张图片 
            ReadImgTask task = new ReadImgTask(); 
            task.execute(String.valueOf(i)); 
        } 
        Toast.makeText(getApplicationContext(), "PreReading...", Toast.LENGTH_LONG).show(); 
        handler.postDelayed(new Runnable() { 
             
            @Override 
            public void run() { 
                startActivity(new Intent(WelcomeActivity.this, MainActivity.class));//启动MainActivity 
                 
                finish(); 
            } 
        }, 3000);//显示三秒钟的欢迎界面 
    } 
     
    class ReadImgTask extends PreReadTask<String, Void, Void> { 
 
        @Override 
        protected Void doInBackground(String... arg0) { 
            try { 
                Thread.sleep(200);//模拟网络延时 
            } catch (InterruptedException e) { 
                // TODO Auto-generated catch block 
                e.printStackTrace(); 
            } 
            Data.putData(arg0[0], BitmapFactory.decodeResource(getResources(), R.drawable.icon));//把预读的数据放到hashmap中 
            return null; 
        } 
         
    } 
     


MainActivity.java 主界面,有一个listview,listview中显示图片和文字,模拟图片从网络异步获取
Java代码 
package cn.caiwb 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.List; 
import java.util.ListIterator; 
import java.util.Map; 
import android.app.Activity; 
import android.app.AlertDialog; 
import android.app.Dialog; 
import android.app.ListActivity; 
import android.app.ProgressDialog; 
import android.content.Context; 
import android.content.DialogInterface; 
import android.content.Intent; 
import android.database.Cursor; 
import android.graphics.Bitmap; 
import android.graphics.BitmapFactory; 
import android.os.AsyncTask; 
import android.os.Bundle; 
import android.provider.ContactsContract; 
import android.util.Log; 
import android.view.LayoutInflater; 
import android.view.View; 
import android.view.ViewGroup; 
import android.widget.AbsListView; 
import android.widget.AbsListView.OnScrollListener; 
import android.widget.Adapter; 
import android.widget.AdapterView; 
import android.widget.AdapterView.OnItemClickListener; 
import android.widget.BaseAdapter; 
import android.widget.GridView; 
import android.widget.ImageView; 
import android.widget.ListAdapter; 
import android.widget.ListView; 
import android.widget.SimpleAdapter; 
import android.widget.TextView; 
import android.widget.Toast; 
 
public class MainActivity extends Activity { 
     
     
    private ListView mListView; 
    private List<HashMap<String, Object>> mData; 
     
    private BaseAdapter mAdapter; 
     
     
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.main); 
        mListView = (ListView) findViewById(R.id.listview); 
        mData = new ArrayList<HashMap<String,Object>>(); 
        mAdapter = new CustomAdapter(); 
         
        mListView.setAdapter(mAdapter); 
        for(int i = 0; i < 100; i++) {//初始化100项数据 
            HashMap data = new HashMap<String, Object>(); 
            data.put("title", "title" + i); 
            mData.add(data); 
        } 
    } 
     
     
 
 
    class CustomAdapter extends BaseAdapter { 
 
         
        CustomAdapter() { 
             
        } 
         
        @Override 
        public int getCount() { 
            return mData.size(); 
        } 
 
        @Override 
        public Object getItem(int position) { 
            return mData.get(position); 
        } 
 
        @Override 
        public long getItemId(int position) { 
            return 0; 
        } 
 
        @Override 
        public View getView(int position, View convertView, ViewGroup parent) { 
            View view = convertView; 
            ViewHolder vh; 
            if(view == null) { 
                view = LayoutInflater.from(MainActivity.this).inflate(R.layout.list_item, null); 
                vh = new ViewHolder(); 
                vh.tv = (TextView) view.findViewById(R.id.textView); 
                vh.iv = (ImageView) view.findViewById(R.id.imageView); 
                view.setTag(vh); 
            } 
            vh = (ViewHolder) view.getTag(); 
            vh.tv.setText((String) mData.get(position).get("title")); 
            Bitmap bitmap = (Bitmap) mData.get(position).get("pic"); 
            if(bitmap != null) { 
                vh.iv.setImageBitmap(bitmap); 
            } 
            else { 
                vh.iv.setImageBitmap(null); 
            } 
             
            AsyncTask task = (AsyncTask) mData.get(position).get("task"); 
            if(task == null || task.isCancelled()) { 
                mData.get(position).put("task", new GetItemImageTask(position).execute(null));//启动线程异步获取图片 
            } 
             
            return view; 
        } 
 
         
         
    } 
     
    static class ViewHolder { 
        TextView tv; 
        ImageView iv; 
    } 
     
     
    class GetItemImageTask extends AsyncTask<Void, Void, Void> {//获取图片仍采用AsyncTask,这里的优化放到下篇再讨论 
         
        int id; 
         
        GetItemImageTask(int id) { 
            this.id = id; 
        } 
 
        @Override 
        protected Void doInBackground(Void... params) { 
             
            Bitmap bm = (Bitmap) Data.getData(String.valueOf(id)); 
            if(bm != null) {//如果hashmap中已经有数据, 
                mData.get(id).put("pic", bm); 
            } 
            else {//模拟从网络获取 
                try { 
                    Thread.sleep(200);//模拟网络延时 
                } catch (InterruptedException e) { 
                    e.printStackTrace(); 
                } 
                mData.get(id).put("pic", BitmapFactory.decodeResource(getResources(), R.drawable.icon)); 
            } 
            return null; 
        } 
         
        protected void onPostExecute (Void result) { 
            mAdapter.notifyDataSetChanged(); 
        } 
         
    } 
     


Data.java 静态的Hashmap保存预读数据

Java代码 
package cn.caiwb; 
 
import java.util.AbstractMap; 
import java.util.HashMap; 
import java.util.concurrent.ConcurrentHashMap; 
 
public class Data { 
    private static AbstractMap<String, Object> mData = new ConcurrentHashMap<String, Object>(); 
     
    private Data() { 
         
    } 
     
    public static void putData(String key,Object obj) { 
        mData.put(key, obj); 
    } 
     
    public static Object getData(String key) { 
        return mData.get(key); 
    } 




运行结果:





从执行结果可以看到,当进入MainActivity时,listview中的第一屏的图片已经加载好了。

这个简单例子中还不能很好地体现预读带来的用户体验的优势,不过一些应用(如前面提到过的新闻阅读类应用),实现了预读机制,使响应性大大提高,增强了用户体验。

总结:

1、通过实现自定义的AsyncTask来避免AsyncTask引起的FC风险和满足特定的后台异步任务需求

2、实现后台预读可以提高应用的响应性。

3、使用Executors.newSingleThreadExecutor()创建只有一个工作队列的线程池来实现预读需求。

注意:

1、预读队列的工作线程可以不止一个,请根据需求配置自己的线程池。

2、adapter的getview()方法中,我们仍然采用了AsyncTask实现异步获取图片,以后我们将探讨更好的解决办法,在提高响应性的同时,避免了AyncTask带来的FC风险

3、预读也要控制成本,存储空间、耗电和流量都是要考虑的因素
  • 大小: 23 KB
分享到:
评论

相关推荐

    Android多线程任务优化2:实现后台预读线程

    本篇文章将深入探讨如何通过实现后台预读线程来优化多线程任务,提高用户界面的响应速度和整体应用体验。 首先,了解什么是预读。预读是一种策略,它在用户实际需要数据之前就开始加载,旨在减少等待时间,提高流畅...

    Android TXT阅读器 源代码

    通过分析和学习这个Android TXT阅读器的源代码,开发者可以深入了解Android系统的文件操作、UI设计、多线程编程等关键技能,同时也能掌握编码转换的实践方法,提升自己的Android开发能力。在实践中不断探索和改进,...

    百度tts语音合成demo Android APP

    3. **UI线程与多线程**:官方Demo中通常会建议在后台线程执行耗时操作,以避免阻塞UI线程导致应用无响应。然而,此APP选择在UI线程内执行语音合成,这可能会使主线程负载加重,影响用户界面的响应速度。如果合成过程...

    Android图片声音显示

    - **AsyncTask**: 用于执行后台任务,常用于加载大图片或播放音频,避免阻塞UI线程。 - **Handler/Looper**: 用于在后台线程与主线程之间传递消息,更新UI。 - **RxJava/RxAndroid**: 提供了一种基于观察者...

    android音乐播放器源码

    在Android平台上开发一款音乐播放器是一项复杂而有趣的任务,它涉及到多媒体处理、用户界面设计以及系统服务等多个领域的技术。这个"android音乐播放器源码"项目涵盖了以下关键知识点: 1. **多媒体框架...

    Android应用源码之边下载边播music.zip

    - **Activity和Service**:Android应用的核心组件,Activity负责用户界面交互,Service则用于在后台执行长时间运行的任务,如音乐下载和播放。 - **Intent**:Android中的意图(Intent)用于在组件之间传递消息,...

    Android-VolleyHttp网络访问框架

    3. **NetworkDispatcher**:在后台线程中处理网络请求,将请求发送到服务器并接收响应。 4. **CacheDispatcher**:负责处理缓存策略,从本地缓存中获取数据或把需要缓存的响应放入缓存。 5. **ResponseDelivery**:...

    采用MediaPlayer播放网络音频和本地音频(子线程里快速启动/切换播放音频)

    - 默认情况下,Android系统建议在主线程之外处理多媒体播放,以避免阻塞UI线程。因此,在子线程中使用MediaPlayer可以提高应用的响应速度,防止用户界面冻结。 2. **在线播放音频**: - MediaPlayer支持HTTP或...

    ux操作系统性能调优的方法

    内核参数的优化涉及到多个方面,例如,可以调整TCP/IP网络参数以改善网络性能,或优化内存分配策略以提高内存利用率。 4. **处理器子系统调优** 通过调整调度算法、进程优先级等,可以优化处理器子系统。例如,...

    android 音乐

    在Android平台上,开发一款音乐播放器,如"星雨音乐",涉及到许多核心技术和...以上是开发"星雨音乐"这样的Android音乐播放器所涉及的主要知识点,每一个环节都需要精心设计和实现,才能打造一款用户喜爱的音乐应用。

    Android应用开发记录-字幕播放器(3)工程包

    在Android应用开发中,创建一个字幕播放器是一项复杂但重要的任务,特别是在视频播放领域。本项目"Android应用开发记录-字幕播放器(3)工程包"专注于使用`Service`来实现音频播放功能,结合了`MediaPlayer`和`Proxy...

    安卓音乐播放器(代码)

    【安卓音乐播放器(代码)】是一个基础的音乐播放应用的源码实现,适用于学习和理解Android平台上如何构建一个简单的音乐播放器。虽然它没有包含歌词显示功能,但该代码库提供了基本的音乐播放、控制和管理的核心...

    okio,面向java的现代i/o api.zip

    Okio 是一个面向 Java 的现代 I/O API,它由 Square 公司开发并维护,作为一个开源项目,...通过深入研究 Okio 的设计和实现,开发者可以学习到更多关于 I/O 缓存、并发处理和性能优化的知识,从而提升自己的编程技能。

Global site tag (gtag.js) - Google Analytics