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

来源:http://www.cnblogs.com/jycboy/archive/2016/12/24/save_state_data.html
-Advertisement-
Play Games

一、概述 運行時變更就是設備在運行時發生變化(例如屏幕旋轉、鍵盤可用性及語言)。發生這些變化,Android會重啟Activity,這時就需要保存activity的狀態及與activity相關的任務,以便恢復activity的狀態。 為此,google提供了三種解決方案: 下麵會逐一介紹三種情況,其 ...


一、概述

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

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

  1. 對於少量數據: 通過onSaveInstanceState(),保存有關應用狀態的數據。 然後在 onCreate() 或 onRestoreInstanceState() 期間恢復 Activity 狀態。
  2. 對於大量數據:用 Fragment 保留需要回覆的對象。
  3. 自行處理配置變更,不重啟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 以保留有狀態的對象。

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

  1. 擴展 Fragment 類並聲明對有狀態對象的引用。
  2. 在創建片段後調用 setRetainInstance(boolean)
  3. 將片段添加到 Activity。
  4. 重啟 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 綁定的對象,例如,DrawableAdapterView 或其他任何與 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 boolean isCompleted;
	/**
	 * 進度框
	 */
	private LoadingDialog mLoadingDialog;
	private List<String> items;

	/**
	 * 開始時,顯示載入框
	 */
	@Override
	protected void onPreExecute()
	{
		mLoadingDialog = new LoadingDialog();
		FixProblemsActivity 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)
	{
		if(activity == null){
			mLoadingDialog.dismiss();
			return;
		}
		weakReference = new WeakReference<>(activity);
		// 設置為當前的Activity
		FixProblemsActivity fActivity = (FixProblemsActivity) weakReference.get();
		// 開啟一個與當前Activity綁定的等待框
		if (activity != null && !isCompleted)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(fActivity.getFragmentManager(), "LOADING");
		}
		// 如果完成,通知Activity
		if (isCompleted)
		{
			notifyActivityTaskCompleted();
		}
	}

	/**
	 * 在Activity不可見時,關閉dialog
	 */
    public void dialogDismiss(){
		if(mLoadingDialog != null){
			mLoadingDialog.dismiss();
		}
	}
	private void notifyActivityTaskCompleted()
	{
		if (null != weakReference.get())
		{
			((FixProblemsActivity) weakReference.get()).onTaskCompleted();
		}
	}

}

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

在onPause中關閉dialog,防止記憶體泄漏。

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

效果圖:

 

 

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

源碼下載:http://download.csdn.net/detail/jycboy/9720486  

轉載請註明出處:http://www.cnblogs.com/jycboy/p/save_state_data.html 

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

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 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JavaScript [1]事件 ①用戶操作網頁或者瀏覽器所發生的交互行為稱為事件。比如:點擊按鈕,最小化視窗,修改文本框內容等。 ②JS為我們定義許多瀏覽器中的事件。比如:單擊(onclick)、雙擊(ondblclick)、移動(onmousemove) 等。 ③我們可以通過為事件設置一個響應函 ...
  • 用戶反饋是小程式開發必要的一個功能,但是和自己核心業務沒關係,主要是產品運營方便收集用戶的對產品的反饋。HotApp推出了用戶反饋的組件,方便大家直接集成使用 源碼下載地址: https://github.com/hotapp8/hotapp-fedback (1)零代碼接入用戶反饋功能 界面仿微信 ...
  • 昨天的(今天凌晨)的博文《Android中Fragment和ViewPager那點事兒》中,我們通過使用Fragment和ViewPager模仿實現了微信的佈局框架。今天我們來通過使用ListView實現其中聯繫人一欄的基本視圖,效果如下: 要實現上圖的效果,我們要用到兩個知識點: 1、這裡我們使用 ...
  • listview經常結合下來刷新和上拉載入更多使用,本文總結了三種常用到的方案分別作出說明。 ...
  • 概述 本篇文章會從源碼(基於Android 6.0)角度分析Android中View的繪製流程,側重於對整體流程的分析,對一些難以理解的點加以重點闡述,目的是把View繪製的整個流程把握好,而對於特定實現細節則可以日後再對相應源碼進行研讀。在進行實際的分析之前,我們先來看下麵這張圖: 我們來對上圖做 ...
  • 鎖終端 輸入: <1>cd /Applications/Xcode.app 回車 結果顯示: Xcode.app 輸入: <2>sudo chown -hR root:wheel Contents 回車 結果顯示: WARNING: Improper use of the sudo command ...
  • 經過努力終於發現了最新的 解決cocoaPods安裝的辦法: taobao Gems 源已停止維護,現由 ruby-china 提供鏡像服務 第一步:安裝rvm, 不管需不需要升級ruby,rvm可以讓你擁有多個版本的Ruby,並且可以在多個版本之間自由切換。如果已經安裝過跳到第2步(rvm -v ...
  • 在之前的博文《Android中使用ViewPager實現屏幕頁面切換和引導頁效果實現》和《Android中Fragment的兩種創建方式》以及《Android中Fragment與Activity之間的交互(兩種實現方式)》中我們介紹了ViewPager以及Fragment各自的使用場景以及不同的實現 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...