Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android屏幕旋轉 處理Activity與AsyncTask的最佳解決方案

Android屏幕旋轉 處理Activity與AsyncTask的最佳解決方案

編輯:關於Android編程

一、概述

運行時變更就是設備在運行時發生變化(例如屏幕旋轉、鍵盤可用性及語言)。發生這些變化,Android會重啟Activity,這時就需要保存activity的狀態及與activity相關的任務,以便恢復activity的狀態。

為此,google提供了三種解決方案:

  • 對於少量數據: 通過onSaveInstanceState(),保存有關應用狀態的數據。 然後在 onCreate() 或 onRestoreInstanceState() 期間恢復 Activity 狀態。
  • 對於大量數據:用 Fragment 保留需要回復的對象。
  • 自行處理配置變更,不重啟Activity。

下面會逐一介紹三種情況,其實保存一些變量對象很簡單,難的是當Activity創建異步線程去加載數據時,旋轉屏幕時,怎麼保存線程的狀態。比如,在線程的加載過程中,旋轉屏幕,就會存在問題:此時數據沒有完成加載,onCreate重新啟動時,會再次啟動線程;而上個線程可能還在運行,並且可能會更新已經不存在的控件,造成錯誤。下面會一一解決這些問題。本文較長,主要是代碼多,可以先下載demo,源碼下載:http://download.csdn.net/detail/jycboy/9720486對比著看。

 二、使用onSaveInstanceState,onRestoreInstanceState​

代碼如下:

/**
 * 使用onSaveInstanceState,onRestoreInstanceState;
 * 在這裡不考慮沒有加載完畢,就旋轉屏幕的情況。
 * @author 超超boy
 *
 */
public class SavedInstanceStateActivity extends ListActivity
{
 private static final String TAG = "MainActivity";
 private ListAdapter mAdapter;
 private ArrayList<String> mDatas;
 private DialogFragment mLoadingDialog;
 private LoadDataAsyncTask mLoadDataAsyncTask;
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 Log.e(TAG, "onCreate");
 initData(savedInstanceState);
 }

 /**
 * 初始化數據
 */
 private void initData(Bundle savedInstanceState)
 {
 if (savedInstanceState != null)
 mDatas = savedInstanceState.getStringArrayList("mDatas");
 if (mDatas == null)
 {
 mLoadingDialog = new LoadingDialog();
 mLoadingDialog.show(getFragmentManager(), "LoadingDialog");
 mLoadDataAsyncTask = new LoadDataAsyncTask();
 mLoadDataAsyncTask.execute();
 //mLoadDataAsyncTas
 } else
 {
 initAdapter();
 }
 }
 /**
 * 初始化適配器
 */
 private void initAdapter()
 {
 mAdapter = new ArrayAdapter<String>(
 SavedInstanceStateActivity.this,
 android.R.layout.simple_list_item_1, mDatas);
 setListAdapter(mAdapter);
 }
 @Override
 protected void onRestoreInstanceState(Bundle state)
 {
 super.onRestoreInstanceState(state);
 Log.e(TAG, "onRestoreInstanceState");
 }
 @Override
 //在這裡保存數據,好用於返回
 protected void onSaveInstanceState(Bundle outState)
 {
 super.onSaveInstanceState(outState);
 Log.e(TAG, "onSaveInstanceState");
 outState.putSerializable("mDatas", mDatas);
 }
 /**
 * 模擬耗時操作
 *
 * @return
 */
 private ArrayList<String> generateTimeConsumingDatas()
 {
 try
 {
 Thread.sleep(3000);
 } catch (InterruptedException e)
 { e.printStackTrace();
 }
 return new ArrayList<String>(Arrays.asList("通過Fragment保存大量數據",
 "onSaveInstanceState保存數據",
 "getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop",
 "Spark"));
 }
 private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void>
 {
 @Override
 protected Void doInBackground(Void... params)
 {
 mDatas = generateTimeConsumingDatas();
 return null;
 }
 @Override
 protected void onPostExecute(Void result)
 {
 mLoadingDialog.dismiss();
 initAdapter();
 }
 }
 @Override
 protected void onDestroy()
 {
 Log.e(TAG, "onDestroy");
 super.onDestroy();
 }
}

界面為一個ListView,onCreate中啟動一個異步任務去加載數據,這裡使用Thread.sleep模擬了一個耗時操作;當用戶旋轉屏幕發生重新啟動時,會onSaveInstanceState中進行數據的存儲,在onCreate中對數據進行恢復,免去了不必要的再加載一遍。

運行結果:

12-24 20:13:41.814 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.124 1994-1994/? E/MainActivity: onSaveInstanceState
12-24 20:13:46.124 1994-1994/? E/MainActivity: onDestroy
12-24 20:13:46.154 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.164 1994-1994/? E/MainActivity: onRestoreInstanceState

當正常加載數據完成之後,用戶不斷進行旋轉屏幕,log會不斷打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,驗證Activity重新啟動,但是我們沒有再次去進行數據加載。

如果在加載的時候,進行旋轉,則會發生錯誤,異常退出(退出原因:dialog.dismiss()時發生NullPointException,因為與當前對話框綁定的FragmentManager為null,在這裡這個不是關鍵)。

效果圖:

三、使用Fragment保留對象,恢復數據

果重啟 Activity 需要恢復大量數據、重新建立網絡連接或執行其他密集操作,依靠系統通過onSaveInstanceState() 回調為您保存的 Bundle,可能無法完全恢復 Activity 狀態,因為它並非設計用於攜帶大型對象(例如位圖),而且其中的數據必須先序列化,再進行反序列化,這可能會消耗大量內存並使得配置變更速度緩慢。 在這種情況下,如果 Activity 因配置變更而重啟,則可通過保留 Fragment 來減輕重新初始化 Activity 的負擔。此片段可能包含對您要保留的有狀態對象的引用。

當 Android 系統因配置變更而關閉 Activity 時,不會銷毀您已標記為要保留的 Activity 的片段。 您可以將此類片段添加到 Activity 以保留有狀態的對象。

要在運行時配置變更期間將有狀態的對象保留在片段中,請執行以下操作:

  • 擴展 Fragment 類並聲明對有狀態對象的引用。
  • 在創建片段後調用 setRetainInstance(boolean)。
  • 將片段添加到 Activity。
  • 重啟 Activity 後,使用 FragmentManager 檢索片段。

例如,按如下方式定義片段:

public class RetainedFragment extends Fragment {
 // data object we want to retain
 private MyDataObject data;
 // this method is only called once for this fragment
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // retain this fragment
 setRetainInstance(true);
 }
 public void setData(MyDataObject data) {
 this.data = data;
 }
 public MyDataObject getData() {
 return data;
 }
}

注意:盡管您可以存儲任何對象,但是切勿傳遞與 Activity 綁定的對象,例如,Drawable、Adapter、View 或其他任何與 Context 關聯的對象。否則,它將使Activity無法被回收造成內存洩漏。(洩漏資源意味著應用將繼續持有這些資源,但是無法對其進行垃圾回收,因此可能會丟失大量內存)

下面舉一個實際的例子:

1.RetainedFragment

public class RetainedFragment extends Fragment
{
 // data object we want to retain
 private Bitmap data;
 // this method is only called once for this fragment
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 // retain this fragment
 setRetainInstance(true);
 }
 public void setData(Bitmap data)
 {
 this.data = data;
 }
 public Bitmap getData()
 {
 return data;
 }
}

只是保持Bitmap對象的引用,你可以用Fragment保存多個對象。

2.FragmentRetainDataActivity:

public class FragmentRetainDataActivity extends Activity
{
 private static final String TAG = "FragmentRetainData";
 private RetainedFragment dataFragment;
 private DialogFragment mLoadingDialog;
 private ImageView mImageView;
 private Bitmap mBitmap;
 BitmapWorkerTask bitmapWorkerTask;
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Log.e(TAG, "onCreate");
 // find the retained fragment on activity restarts
 FragmentManager fm = getFragmentManager();
 dataFragment = (RetainedFragment) fm.findFragmentByTag("data");
 // create the fragment and data the first time
 if (dataFragment == null)
 {
 // add the fragment
 dataFragment = new RetainedFragment();
 fm.beginTransaction().add(dataFragment, "data").commit();
 }
 // the data is available in dataFragment.getData()
 mBitmap = dataFragment.getData();
 initView();
 }
 /**
 * 初始化控件
 */
 private void initView()
 {
 mImageView = (ImageView) findViewById(R.id.id_imageView);
 if(mBitmap != null)
 mImageView.setImageBitmap(mBitmap);
 //圖片為空時,加載圖片;有時候即使dataFragment!=null時,圖片也不一定就加載完了,比如在加載的過程中,旋轉屏幕,此時圖片就沒有加載完
 else{
 mLoadingDialog = new LoadingDialog();
 mLoadingDialog.show(getFragmentManager(), "LOADING_DIALOG");
 bitmapWorkerTask = new BitmapWorkerTask(this);
 bitmapWorkerTask.execute("http://images2015.cnblogs.com/blog/747969/201612/747969-20161222164357995-1098775233.jpg");
 }
 }
 /**
 * 異步下載圖片的任務。
 * 設置成靜態內部類是為了防止內存洩漏
 * @author guolin
 */
 private static class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
 //圖片的URL地址
 private String imageUrl;
 //保存外部activity的弱引用
 private WeakReference<Context> weakReference;
 public BitmapWorkerTask(Context context) {
 weakReference = new WeakReference<>(context);
 }
 @Override
 protected Bitmap doInBackground(String... params) {
 imageUrl = params[0];
 //為了演示加載過程,阻塞2秒
 try
 {Thread.sleep(2000);
 } catch (InterruptedException e)
 { e.printStackTrace();
 }
 return downloadUrlToStream(imageUrl);
 }
 @Override
 protected void onPostExecute(Bitmap bitmap) {
 super.onPostExecute(bitmap);
 if(bitmap !=null){
 FragmentRetainDataActivity retainDataActivity= (FragmentRetainDataActivity) weakReference.get();
 //調用回調方法
 retainDataActivity.onLoaded(bitmap);
 }
 }
 /**
 * 建立HTTP請求,並獲取Bitmap對象。
 * 修改了下
 * @param urlString
 * 圖片的URL地址
 * @return 解析後的Bitmap對象
 */
 private Bitmap downloadUrlToStream(String urlString) {
 HttpURLConnection urlConnection = null;
 Bitmap bitmap = null;
 try {
 final URL url = new URL(urlString);
 urlConnection = (HttpURLConnection) url.openConnection();
 if(urlConnection.getResponseCode()==HttpURLConnection.HTTP_OK){ //連接成功
  InputStream is = urlConnection.getInputStream();
  bitmap = BitmapFactory.decodeStream(is);
  is.close();
  return bitmap;
 }else{
  return null;
 }
 } catch (final IOException e) {
 e.printStackTrace();
 } finally {
 if (urlConnection != null) {
  urlConnection.disconnect();
 }
 }
 return null;
 }
 }
 //加載完畢的回掉
 public void onLoaded(Bitmap bitmap){
 mBitmap = bitmap;
 mLoadingDialog.dismiss();
 mImageView.setImageBitmap(mBitmap);
 // load the data from the web
 dataFragment.setData(mBitmap);
 Log.e(TAG, "onLoaded");
 }
 public void onPause(){
 super.onPause();
 Log.e(TAG, "onPause");
 if(getFragmentManager() != null && mLoadingDialog != null)
 mLoadingDialog.dismiss();
 }
 @Override
 public void onDestroy()
 {
 super.onDestroy();
 Log.e(TAG, "onDestroy");
 if(bitmapWorkerTask !=null)
 bitmapWorkerTask.cancel(true);
 // store the data in the fragment
 dataFragment.setData(mBitmap);
 }
}

這裡邊用BitmapWorkerTask異步下載圖片,downloadUrlToStream封裝了下載圖片的代碼;

BitmapWorkerTask用弱引用保持外部Activity對象防止內存洩漏,下載完畢後用onLoaded回調方法更新UI。

通過檢查dataFragment、mBitmap判斷是否已經加載過,加載過直接用就可以。

效果圖:

在gif裡可以看到,如果未加載完畢就旋轉屏幕,它會重新啟動異步線程去下載,這種效果並不好,我們會在最後解決這個問題。

四、自行處理配置變更

如果應用在特定配置變更期間無需更新資源,並且因性能限制您需要盡量避免重啟,則可聲明 Activity 將自行處理配置變更,這樣可以阻止系統重啟 Activity。

要聲明由 Activity 處理配置變更,需設置清單文件manifest:

<activity android:name=".MyActivity" android:configChanges="orientation|keyboardHidden"
 android:label="@string/app_name">

"orientation" 和 "keyboardHidden",分別用於避免因屏幕方向和可用鍵盤改變而導致重啟)。您可以在該屬性中聲明多個配置值,方法是用管道 | 字符分隔這些配置值。

注意:API 級別 13 或更高版本的應用時,若要避免由於設備方向改變而導致運行時重啟,則除了 "orientation" 值以外,您還必須添加 "screenSize" 值。 也就是說,您必須聲明 android:configChanges="orientation|screenSize"。

當其中一個配置發生變化時,MyActivity 不會重啟。相反,MyActivity 會收到對onConfigurationChanged() 的調用。向此方法傳遞Configuration 對象指定新設備配置。您可以通過讀取Configuration 中的字段,確定新配置,然後通過更新界面中使用的資源進行適當的更改。

例如,以下onConfigurationChanged() 實現檢查當前設備方向:

@Override
public void onConfigurationChanged(Configuration newConfig) {
 super.onConfigurationChanged(newConfig);
 
 // Checks the orientation of the screen
 if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
 Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
 } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
 Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
 }
}

示例代碼:

public class ConfigChangesTestActivity extends ListActivity
{
 private static final String TAG = "MainActivity";
 private ListAdapter mAdapter;
 private ArrayList<String> mDatas;
 private DialogFragment mLoadingDialog;
 private LoadDataAsyncTask mLoadDataAsyncTask;
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 Log.e(TAG, "onCreate");
 initData(savedInstanceState);
 }
 /**
 * 初始化數據
 */
 private void initData(Bundle savedInstanceState)
 {
 mLoadingDialog = new LoadingDialog();
 mLoadingDialog.show(getFragmentManager(), "LoadingDialog");
 mLoadDataAsyncTask = new LoadDataAsyncTask();
 mLoadDataAsyncTask.execute();
 }
 /**
 * 初始化適配器
 */
 private void initAdapter()
 {
 mAdapter = new ArrayAdapter<String>(ConfigChangesTestActivity.this,
 android.R.layout.simple_list_item_1, mDatas);
 setListAdapter(mAdapter);
 }
 /**
 * 模擬耗時操作
 *
 * @return
 */
 private ArrayList<String> generateTimeConsumingDatas()
 {
 try
 {
 Thread.sleep(2000);
 } catch (InterruptedException e)
 {
 }
 return new ArrayList<String>(Arrays.asList("通過Fragment保存大量數據",
 "onSaveInstanceState保存數據",
 "getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop",
 "Spark"));
 }
 /**
 * 當配置發生變化時,不會重新啟動Activity。但是會回調此方法,用戶自行進行對屏幕旋轉後進行處理
 */
 @Override
 public void onConfigurationChanged(Configuration newConfig)
 {
 super.onConfigurationChanged(newConfig);
 if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
 {
 Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
 } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT)
 {
 Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
 }
 }
 private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void>
 {
 @Override
 protected Void doInBackground(Void... params)
 {
 mDatas = generateTimeConsumingDatas();
 return null;
 }
 @Override
 protected void onPostExecute(Void result)
 {
 mLoadingDialog.dismiss();
 initAdapter();
 }
 }
 @Override
 protected void onDestroy()
 {
 Log.e(TAG, "onDestroy");
 super.onDestroy();
 }
}

這種方法使用簡單,在回調方法onConfigurationChanged執行你的操作就可以,不會重啟Activity。

但是自行處理配置變可能導致備用資源的使用更為困難,因此不到萬不得已不用,大部分情況推薦用Fragment。

效果圖:

五、旋轉屏幕時如果任務線程未執行完,如何保存恢復

解決上邊提到的問題,在未加載完畢的情況下,旋轉屏幕,保存任務線程,重啟activity不重新執行異步線程。那麼他的難點就是保存任務線程,不銷毀它,用上邊三個方法都可以實現,推薦用1,2種方法。下邊就主要介紹用第二種方法Fragment,那麼你也肯定想到了,用Fragment保存AsyncTask就可以了。

代碼如下:

1. OtherRetainedFragment

/**
 * 保存對象的Fragment
 * @author 超超boy
 *
 */
public class OtherRetainedFragment extends Fragment
{
 // data object we want to retain
 // 保存一個異步的任務
 private MyAsyncTask data;
 // this method is only called once for this fragment
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 // retain this fragment
 setRetainInstance(true);
 }
 public void setData(MyAsyncTask data)
 {
 this.data = data;
 }
 public MyAsyncTask getData()
 {
 return data;
 }

和之前差不多就是保存對象是MyAsyncTask。

2.MyAsyncTask:

public class MyAsyncTask extends AsyncTask<Void, Void, Void>
{
 //保存外部activity的弱引用
 private WeakReference<Context> weakReference;
 public MyAsyncTask(Context context) {
 weakReference = new WeakReference<>(context);
 }
 private FixProblemsActivity activity;
 /**
 * 是否完成
 */
 private boolean isCompleted;
 /**
 * 進度框
 */
 private LoadingDialog mLoadingDialog;
 private List<String> items;
 /**
 * 開始時,顯示加載框
 */
 @Override
 protected void onPreExecute()
 {
 mLoadingDialog = new LoadingDialog();
 activity = (FixProblemsActivity) weakReference.get();
 if(activity != null)
 mLoadingDialog.show(activity.getFragmentManager(), "LOADING");
 }
 /**
 * 加載數據
 */
 @Override
 protected Void doInBackground(Void... params)
 {
 items = loadingData();
 return null;
 }
 /**
 * 加載完成回調當前的Activity
 */
 @Override
 protected void onPostExecute(Void unused)
 {
 isCompleted = true;
 notifyActivityTaskCompleted();
 if (mLoadingDialog != null)
 mLoadingDialog.dismiss();
 }
 public List<String> getItems()
 {
 return items;
 }
 private List<String> loadingData()
 {
 try
 {
 Thread.sleep(5000);
 } catch (InterruptedException e)
 {
 }
 return new ArrayList<String>(Arrays.asList("通過Fragment保存大量數據",
 "onSaveInstanceState保存數據",
 "getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop",
 "Spark"));
 }
 /**
 * 設置Activity,因為Activity會一直變化
 *
 * @param activity
 */
 public void setActivity(Context activity)
 { weakReference = new WeakReference<>(activity);
 // 設置為當前的Activity
 this.activity = (FixProblemsActivity) activity;
 // 開啟一個與當前Activity綁定的等待框
 if (activity != null && !isCompleted)
 {
 mLoadingDialog = new LoadingDialog();
 mLoadingDialog.show(this.activity.getFragmentManager(), "LOADING");
 }
 // 如果完成,通知Activity
 if (isCompleted)
 {
 notifyActivityTaskCompleted();
 }
 }
 /**
 * 在Activity不可見時,關閉dialog
 */
 public void dialogDismiss(){
 if(mLoadingDialog != null){
 mLoadingDialog.dismiss();
 }
 }
 private void notifyActivityTaskCompleted()
 {
 if (null != activity)
 {
 activity.onTaskCompleted();
 }
 }
}
  • 和之前一樣保留Activity的虛引用,防止內存洩漏。
  • setActivity()方法用於任務未完成時,重啟activity相應的創建一個新的dialog。
  • dialogDismiss用於在在Activity不可見時,關閉dialog,防止以前的dialog造成內存洩漏。
  • 當任務完成時,調用activity的回調方法onTaskCompleted更新UI。

3.FixProblemsActivity

public class FixProblemsActivity extends ListActivity
{
 private static final String TAG = "MainActivity";
 private ListAdapter mAdapter;
 private List<String> mDatas;
 private OtherRetainedFragment dataFragment;
 private MyAsyncTask mMyTask;
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 Log.e(TAG, "onCreate");
 // find the retained fragment on activity restarts
 FragmentManager fm = getFragmentManager();
 dataFragment = (OtherRetainedFragment) fm.findFragmentByTag("data");
 // create the fragment and data the first time
 if (dataFragment == null)
 {
 // add the fragment
 dataFragment = new OtherRetainedFragment();
 fm.beginTransaction().add(dataFragment, "data").commit();
 }
 mMyTask = dataFragment.getData();
 if (mMyTask != null)
 { //與新的Activity進行綁定
 mMyTask.setActivity(this);
 } else { //啟動一個新的
 mMyTask = new MyAsyncTask(this);
 dataFragment.setData(mMyTask);
 mMyTask.execute();
 }
 // the data is available in dataFragment.getData()
 }
 @Override
 protected void onRestoreInstanceState(Bundle state)
 {
 super.onRestoreInstanceState(state);
 Log.e(TAG, "onRestoreInstanceState");
 }
 @Override
 protected void onDestroy()
 {
 Log.e(TAG, "onDestroy");
 super.onDestroy();
 }
 @Override
 //在這裡關閉Dialog,否則容易造成內存洩漏
 protected void onPause() {
 super.onPause();
 mMyTask.dialogDismiss();
 }
 /**
 * 回調方法,更新UI
 * 這裡如果在加載的過程中按下返回鍵返回主Activity時,會出現異常,setAdapter on a null object reference。因為activity被銷毀,
 * 要解決這個問題,可以監聽返回鍵事件做相應處理。
 */
 public void onTaskCompleted()
 {
 mDatas = mMyTask.getItems();
 mAdapter = new ArrayAdapter<String>(FixProblemsActivity.this,
 android.R.layout.simple_list_item_1, mDatas);
 setListAdapter(mAdapter);
 }
}

主要在onCreate方法中執行一些邏輯判斷,如果沒有開啟任務(第一次進入),開啟任務;如果已經開啟了,調用setActivity(this);

在onPause中關閉dialog,防止內存洩漏。

設置了等待5秒,足夠旋轉三四個來回了~~~~可以看到雖然在不斷的重啟,但是絲毫不影響任務的運行和加載框的顯示~~~~

效果圖:

寫著寫著就這麼晚了,本以為會寫的很快。。中間Androidstudio的錄制視頻的功能還不好使啦,逼得我在手機上下了個。。。好分析到此結束。

查閱資料時的一些參考文檔:

https://developer.android.google.cn/guide/topics/resources/runtime-changes.html#HandlingTheChange

http://blog.csdn.net/lmj623565791/article/details/37936275

https://developer.android.google.cn/guide/components/activities.html

以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持本站!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved