編輯:關於Android編程
懷著無比崇敬的心情翻開了這本書,路漫漫其修遠兮,程序人生,為自己加油!
作為這本書的第一章,主席還是把Activity搬上來了,也確實,和Activity打交道的次數基本上是最多的,而且他的內容和知識點也是很多的,非常值得我們優先把他掌握,Activity中文翻譯過來就是”活動”的意思,但是主席覺得這樣翻譯有些生硬,直接翻譯成“界面”可能更好,的確,Activity主要也是用於UI效果的呈現,不過兩者翻譯都不為過,我們知其意就行了,正常情況下,我們除了Window,Dialog,Toast,我們還能見到的就只有Activity了,他需要setContentView()去綁定一個視圖View作為效果的呈現,然而,作為一本高質量的進階書。肯定不會去圍繞著入門知識講解,本章的側重點在於對Activity使用過程中搞不清楚的概念,生命周期和啟動模式已經IntentFilter的匹配規則分析,畢竟Activity在異常狀態下的生命周期是多樣化的,至於Activity的啟動模式和各種各樣的Flags,更是讓很多人摸不著頭腦,還有隱式啟動Activity中也有著復雜的Intent匹配過程,所以我們還是一步步的去學習下去,真正的了解Activity這個小家伙!
Activity的生命周期,本章主要講解兩個方面
典型情況下的生命周期 異常情況下的生命周期
典型情況是指用戶參與的情況下,Activity所經過的生命周期的變化,異常情況下的話,就有多種可能了,比如系統回收或者由於當前設備的Configuration發生改變從而導致Activity被銷毀重建,異常情況下的生命周期的關注點和典型情況下有些不同,所以要分開來描述才能描述的清楚些
在正常的情況下,生命周期會經歷以下的生命周期
onCreate:表示Activity正在被創建,這是生命周期的第一個方法,在這個方法中,我們可以做一些初始化的工作,比如調用onContentView去加載界面布局資源,初始化Activity所需數據等
onRestart:表示Activity正在重新啟動,一般情況下,當當前Activity從不可見重新變為可見時,onRestart就會被調用,這總情況一般是用戶行為所導致的,比如用戶按home鍵切換到桌面或者用戶打開了一個新的Activity,這時當前的Activity就會被暫停,也就是onPause和onStop方法被執行了,接著用戶又回到了這個Activity,就會出現這種情況
onStart:表示Activity正在被啟動,即將開始,這個時候Activity已經可見了,但是還沒有出現在前台,還無法和用戶交互,這個時候我們可以理解為Activity已經啟動了,但是我們還沒有看見
onResume:表示Activity已經可見了,並且出現在前台,並開始活動了,要注意這個和onStart的對比,這兩個都表示Activity已經可見了,但是onStart的時候Activity還處於後台,onResume的時候Activity才顯示到前台
onPause:表示Activity正在停止,正常情況下,緊接著onStop就會被調用,在特殊情況下,如果這個時候再快速的回到當前Activity,那麼onResume就會被調用,主席的理解是這個情況比較極端,用戶操作很難重現這個場景,此時可以做一些數據存儲,停止動畫等工作,但是注意不要太耗時了,因為這樣會影響到新的Activity的顯示,onPause必須先執行完,新Activity的onResume才會執行
onStop:表示Activity即將停止,同樣可以做一些輕量級的資源回收,但是不要太耗時了
onDestroy:表示Activity即將被銷毀,這是Activity生命周期的最後一個回調,在這裡我們可以做一些最後的回收工作和資源釋放
正常情況下,Activity的常用生命周期用官網的一張圖就足夠表示了
這裡附加幾個說明
1.針對一個特定的Activity,第一次啟動,回調如下:onCreate ——> onStart ——> onResume
2.當用戶打開新的Activity或者切換到桌面的時候,回調如下:onPause ——> onStop ——> 這裡有一種特殊的情況就是,如果新的Activity采取了透明的主題的話,那麼當前Activity不會回調onStop
3.當用戶再次回到原來的Activity,回調如下:onRestart ——> onStart ——> onResume
4.d當用戶按back鍵的時候回調如下:onPause ———> onStZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcCAmbWRhc2g7Jm1kYXNoOyZndDsgb25EZXN0cm95PC9wPg0KPHA+NS61sUFjdGl2aXR5sbvPtc2zu9jK1bXEyrG68tTZtM608r+qo6zJ+sP81tzG2rvYtfe3vbeous0xysfSu9H5tcSjrLWrysfE49Kq16LS4tK7z8K+zcrH1rvKx8n6w/zW3Mba0rvR+aOssru0+rHty/nT0LXEvfizzLa8ysfSu9H5tcSjrNXiuPbOyszitcjPwrvYz+rPuLfWzvY8L3A+DQo8cD42LrTT1fu49sn6w/zW3MbawLTLtaOsb25DcmVhdGW6zW9uRGVzdHJvecrHxeTM17XELLfWsfCx6sq+18VBY3Rpdml0ebXEtLS9qLrNz/q72aOssqLH0ta7v8nE3NPQ0ru0zrX308OjrLTTQWN0aXZpdHnKx7fxv8m8+8C0y7WjrG9uU3RhcnS6zW9uU3RvcMrHxeTM17XEo6zL5tfF08O7p7XEstnX97rNyeixuMbBxLu1xLXjwcG6zc+ow/CjrNXiwb249re9t6i/ycTcsbu199PDtuC0zqOstNNBY3Rpdml0ecrHt/HU2sewzKjAtMu1o6xvblJlc3VtZbrNb25QYXVzZcrHxeTM17XEo6zL5tfF08O7p7LZ1/e78tXfyeixuLXEtePBwbrNz6jD8KOs1eLBvbj2t723qL/JxNyxu7bgtM6199PDPC9wPg0KPHA+1eLA78zhs/bBvbj2zsrM4jwvcD4NCjxwPjEub25TdGFydLrNb25SZXN1bWWjrG9uUGF1c2W6zW9uU3RvcLTTw+jK9snPtryy7rK7tuCjrLbUztLDx8C0y7XT0Mqyw7TKtdbK0NS1xLK7zazE2KO/IDIuvNnJ6LWxx7BBY3Rpdml0ec6qQaOsyOe5+9PDu6e08r+qwcvSu7j20MK1xEFjdGl2aXR5zqpCo6zEx8O0QrXEb25SZXN1bWW6zUG1xG9uUGF1c2XLrc/I1rTQ0MTho788L3A+DQo8cD7O0sPHz8jAtLvYtPC12tK7uPbOyszio6y008q1vMrKudPDuf2zzMC0y7WjrCBvblN0YXJ0us1vblJlc3VtZaOsb25QYXVzZbrNb25TdG9wv7TG8MC0tcTIt7Lusru24KOsyfXWwc7Sw8e/ydLU1ruxo8H0xuTW0LXE0ru21KOsscjI59a7saPB9G9uU3RhcnS6zW9uU3RvcKOsvMjIu8jntMujrMTHzqrKssO0QW5kcm9pZM+1zbO7ubvhzOG5qb+0xvDAtNbYuLS1xL3Tv9rE2KO/uPm+3cnPw+a1xLfWzvajrM7Sw8fWqrXAo6zV4sG9uPbF5LbUtcS72LX3t9ax8LT6se2yu82stcTS4tLlo6xvblN0YXJ0us1vblN0b3DKx7TTQWN0aXZpdHnKx7fxv8m8+9XiuPa9x7bIwLS72LX3tcSjrLP9wcvV4tbWx/ix8KOs1NrKtbzKtcTKudPD1tCjrMO709DG5Mv7w/fP1LXEx/ix8DwvcD4NCjxwPrXatv649s7KzOKjrM7Sw8e+zdKqtNPUtMLrtcS9x7bIwLS31s720tS8sLXDtb294srNwcujrLnY09pBY3Rpdml0ebXEuaTX99StwO274dTasb7K6brz0PjVwr3avfjQ0L2yveKjrNXiwO/O0sPHtPPWwrXEwcu94ry0v8mjrLTTQWN0aXZpdHm1xMb0tq+5/bPMwLS/tKOsztLDx8C0v7TSu8/Cz7XNs7XE1LTC66OsQWN0aXZpdHnG9Lavuf2zzLXE1LTC68/gtbG4tNTTo6zJ6LzGtb3By0luc3RydW1lbnRhdGlvbixBY3Rpdml0us1BY3Rpdml0eU1hbmFnZXJTZXJ2aWNlo6hBTVOjqaOs1eLA77K7z+rPuLfWzvbV4tK7uf2zzKOsvPK1pcDtveKjrMb0tq9BY3Rpdml0ebXEx+vH87vh08kQEBAgSW5zdHJ1bWVudGF0aW9uIMC0tKbA7aOsyLu688v7zai5/UJpbmRlcs/yQU1Tt6LH68fzo6xBTVPE2rK/zqy7pNfF0ru49kFjdGl2aXR5U3RhY2ujrLKiuLrU8NW7xNq1xEFjdGl2aXR5tcTXtMyszayyvaOsQU1Tzai5/UFjdGl2aXR5VGhyZWFkyKXNrLK9QWN0aXZpdHm1xNe0zKy007b4zeqzycn6w/zW3Mbat723qLXEtffTw6Os1NpBY3Rpdml0eVN0YWNr1tC1xHJlc3VtZVRvcEFjdGl2aXR5TG5uZXJMb2NrZWS3vbeo1tCjrNPQ1eLDtNXits60+sLrPC9wPg0KPHByZSBjbGFzcz0="brush:java;"> //we need to start pausing the current activity so the top one can be resumed boolean dontWaitForPause = (next.info.flags& ActivityInfo.FLAG_RESUME_WHILE_PAUSING)!=0; boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, KeyStore.TrustedCertificateEntry,dontWaitForPause); if(mResumedActivity != null){ pausing != startPaUSINGlOCAKED(userLeaving,false,true,dontWaitForPause); if(DEBUG_STATES){ Slog.d(TAG,"resumeTopActivityLocked:pausing" + mResumedActivity); } }
從上述的代碼中我們可以看出,在新Activity啟動之前,棧頂的Activity需要先onPause後,新的Activity才能啟動,最終,在ActvityStackSupervisor中的realStartActivityLocked方法中,會調用如下代碼
app.thread.scheduleLaunchActivity(new Intent(r.intent),r.appToken,System.identityHashCode(r),r.info,new Configuration(mService.mConfiguration) ,r.compat,r.task.voiceInteractor,app.repProcState,r.icicle,r.persistentState,results,new Intents,!andResume,mService.isNextTransitionForward() ,profilerInfo);
我們都知道,在這個app.thread的類型是IApplicationThread的具體實現實在ActivityTread中,所以,這段代碼實際上遇到了ActivityThread當中,,即ApplicationThread的scheduleLaunchActivity方法,而scheduleLaunchActivity方法最終會完成生命周期的調用過程,因此可以得出結論,是舊Activity縣onPause,然後新的Activityy再啟動
至於ApplicationThread的scheduleLaunchActivity方法為什麼會完成新Activity的生命周期,請看接下來的代碼,scheduleLaunchActivty為什麼會完成新的Activty
private void handlerLaunchActivity(ActivityClientRecord r, Intent customIntent){ //if we are getting ready to gc after going to the background,well we are back active so skip it unscheduleGcIdler(); mSomeActivitiesChanged =true; if(r.profilerInfo != null){ mProfiler.setProfiler(r.profilerInfo); mProfiler.startProfiling; } //Make sure we are running with the most recent config handlerConfigurationChanged(null,null); if(localLOGV)Slog.v TAG,"Handling launch of"+r); //在這裡新Activity被創建出來,其onCreate和onStart被調用 Activity a = PerformLaunchActivity(r,customIntent); if(a != null){ r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.start; handlerResumeActivity(r.token,false,r.isForward, !r.activity.mFinished && r.startsNotResumed); } //省略... }
c從上面的分析可以看出,當新的Activity啟動的時候,舊的Activity的onPause方法會先執行,然後才啟動新的Activity,到底是不是這樣尼?我們可以寫一個小栗子來驗證一下,如下是兩個Activity的代碼,在MainActivity中點擊按鈕可以跳轉到SecondActivity,同時為了分析生命周期,我們把log日志也打出來
package com.liuguilin.activitysample; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; public class MainActivity extends AppCompatActivity { public static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btnTo).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(MainActivity.this, SecondActivity.class)); } }); } @Override protected void onPause() { super.onPause(); Log.i(TAG, "onPause"); } @Override protected void onStop() { super.onStop(); Log.i(TAG, "onStop"); } }
package com.liuguilin.activitysample; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; /** * Created by lgl on 16/8/24. */ public class SecondActivity extends AppCompatActivity { private static final String TAG = "SecondActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); Log.i(TAG, "onCreate"); } @Override protected void onStart() { super.onStart(); Log.i(TAG, "onStart"); } @Override protected void onResume() { super.onResume(); Log.i(TAG, "onResume"); } }
這樣我們可以觀察到他的生命周期
通過這個生命周期我們可以觀察到,舊的Activity的onPause先調用,然後新的Activity才啟動,這也證實了我們上面的分析原理,也許有人問,你只是分析了Andorid5.0的源碼,你怎麼所有的版本源碼邏輯都相同,的確,我們不能把所有的版本都概括,但是作為Android的一個運行過程的基本邏輯,隨著版本的更新並不會很大的改變,因為Android也需要兼容性,,不能說在同一個版本上運行有兩種不同的邏輯,那根本不可能,關於這一點,我們要把握一個度,就是對於Android的基本運行機制,的不同,Android不能在onPause中做重量級的操作,因為必須在onPause執行完成以後新的Activity才能Resume,從這一點我們也間接性的證明了我們的結論,通過分析這個問題,我們知道onPause和onStop都不能做耗時的操作,尤其是onPause,這也意味著,我們應當盡量的在onStop中做操作,從而使新的Activity盡快顯示出來並且換到前後台
上一節我們分析的是正常事情下的生命周期,但是我們寫程序也不要理想化,居多的問題就是出在異常情況下,我們知道,Activity除了受用戶操作導致的正常生命周期的調度,同時還會存在一些異常的情況,比如當資源相關的系統配置發生改變以及系統內存不足的時候,Activity就有可能被殺死,下面我們具體來分析下這幾種情況
理解這個問題,首先要對系統的資源加載有一定的了解,這裡就不詳細分析系統資源加載的機制了,但是我們簡單說明一下,拿最簡單的圖片來說,當我們把一張圖片擋在drawable中的時候,就可以通過Resources去獲取這張圖片了,同時為了兼容不同的設備,我們可能還需要在其他一些目錄下放置不同的圖片,比如drawable-xhdpi之類的,當應用程序啟動時,系統會根據當前設備的情況去加載合適的Resources資源,比如說橫屏手機和豎屏手機會拿著兩張不同的圖片(設定了landscape或者portrait狀態下的圖片),比如之前Activity處於豎屏,我們突然旋轉屏幕,由於系統配置發生了改變,在默認情況下,Activity會被銷毀並且重新創建,當然我們也可以阻止系統重新創建我們的Activity
默認情況下,如果我們的Activity不做特殊處理,那麼當系統配置發生改變之後,Activity就會銷毀並且重新創建,可以看圖
當系統配置發生改變的時候,Activity會被銷毀,其onPause,onStop,onDestroy均會被調用,同時由於Activity是異常情況下終止的,系統會調用onSaveInstanceState來保存當前Activity的狀態,這個方法調用的時機是在onStop之前,他和onPause沒有既定的時序關系,他即可能在onPause之前調用,也有可能在之後調用,需要強調的是,這個方法只出現在Activity被異常終止的情況下,正常情況下是不會走這個方法的嗎,當我們onSaveInstanceState保存到Bundler對象作為參數傳遞給onRestoreInstanceState和onCreate方法,因此我們可以通過onRestoreInstanceState和onCreate方法來判斷Activity是否被重建。如果被重建了,我們就取出之前的數據恢復,從時序上來說,onRestoreInstanceState的調用時機應該在onStart之後
同時我們要知道,在onSaveInstanceState和onRestoreInstanceState方法中,系統自動為我們做了一些恢復工作,當Activity在異常情況下需要重新創建時,系統會默認我們保存當前的Activity視圖架構,並且為我們恢復這些數據,比如文本框中用戶輸入的數據,ListView滾動的位置,這些View相關的狀態系統都會默認恢復,具體針對某一個特定的View系統能為們恢復那些數據?我們可以查看View的源碼,和Activity一樣,每一個View都有onSaveInstanceState和onRestoreInstanceState這兩個方法,看一下他們的實現,就能知道系統能夠為每一個View恢復數據
關於保存和恢復View的層次結構,系統的工作流程是這樣的:首先Activity被意外終止時,Activity會調用onSaveInstanceState去保存數據,然後Activity會委托Window去保存數據,接著Window再委托上面的頂級容器去保存數據,頂級容器是一個ViewGroup,一般來說他可能是一個DecorView,最後頂層容器再去一一通知他的子元素來保存數據,這樣整個數據保存過程就完成了,可以發現,這是一種典型的委托思想,上層委托下層,父容器委托子容器,去處理一件事件,這種思想在Android 中有很多的應用,這裡就不再重復介紹了,接下來舉個例子,那TextView來說,我們分析一下他到底保存了那些數據
@Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); // Save state if we are forced to boolean save = mFreezesText; int start = 0; int end = 0; if (mText != null) { start = getSelectionStart(); end = getSelectionEnd(); if (start >= 0 || end >= 0) { // Or save state if there is a selection save = true; } } if (save) { SavedState ss = new SavedState(superState); // XXX Should also save the current scroll position! ss.selStart = start; ss.selEnd = end; if (mText instanceof Spanned) { Spannable sp = new SpannableStringBuilder(mText); if (mEditor != null) { removeMisspelledSpans(sp); sp.removeSpan(mEditor.mSuggestionRangeSpan); } ss.text = sp; } else { ss.text = mText.toString(); } if (isFocused() && start >= 0 && end >= 0) { ss.frozenWithFocus = true; } ss.error = getError(); return ss; } return superState; }
從上述源碼中我們可以看到,TextView為了保存自己的文本選中和文本結構內容,並且通過查看onRestoreInstanceState方法的源碼,可以發現它的確恢復了這些數據,具體源碼就不在貼出,讀者可以自己去看下源碼,下面我們看來看下實際的例子,對比一下Activity正常終止和異常終止的不同,同時驗證一下系統的數據恢復能力,為了方便測試,我們采用了旋轉屏幕來終止Activity,在我們旋轉屏幕以後,Activity被銷毀重建,我們輸入的文本被正確還原了,說明我們的系統能夠正確的做一些View層的分析,我們看下代碼
package com.liuguilin.activitysample; import android.os.Bundle; import android.os.PersistableBundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; public class MainActivity extends AppCompatActivity { public static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { String test = savedInstanceState.getString("extra_test"); Log.i(TAG, test); } } @Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { super.onSaveInstanceState(outState, outPersistentState); Log.i(TAG, "onSaveInstanceState"); outState.putString("extra_test", "test"); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String test = savedInstanceState.getString("extra_test"); Log.i(TAG, test); } }
上面的代碼很簡單,首先我們在onSaveInstanceState中保存一個字符串,然後當我們的Activity被銷毀並且重新創建之後,我們再去獲取之前存儲的字符串,接收的位置可以選擇onRestoreInstanceState或者onCreate,兩者的區別是:onRestoreInstanceState一旦被調用,其參數Bundler savedInstanceState一定有值,我們不用額外的判斷是否為空但是onCreate不行,onCreate如果正常啟動的話,其參數Bundler onSaveInstanceState為null,所以需要一些額外的判斷,這兩個方法我們選擇任意一個都是可以進行數據恢復的,但是關鍵建議我們使用onRestoreInstanceState去恢復數據
Activity銷毀後調用了onSaveInstanceState來保存數據,重新創建以後再onCreate和onRestoreInstanceState中能恢復數據,這個正好證明了我們剛才的分析,針對onSaveInstanceState我們有一點要說明,那就是系統只會在即將被銷毀並且有機會重新顯示的情況下才會去調用它,考慮到這一種情況,當Activity正常銷毀的時候,系統不會調用onSaveInstanceState,因為被銷毀的Activity不可能再次被顯示出來,這句話不好理解,但是我們可以對比一下旋轉屏幕所造成的Activity異常銷毀,這個過程和正常停止的Activity是不一樣的,因為旋轉屏幕之後,Activity被銷毀的同時會立即創建新的Activity實例,這個時候Activcity有機會再次立刻顯示,所以系統進行了數據存儲,這裡可以簡單的這麼理解,系統只在Activity異常終止的情況下才會調用onSaveInstanceState和onRestoreInstanceState來存儲和恢復數據,其他情況不會觸發
這個情況我們不好模擬,但是其數據的存儲和恢復過程和情況一是一致的,這裡我們描述一下Activity的優先級情況,Activity按照優先級的從高往低,可以分為三種:
1.前台Activity:正在和用戶交互的Activity,優先級最高 2.可見但非前台Activity:比如對話框,導致Activity可見但是位於後台無法和用戶直接交互 3.後台Activity:已經被暫停的Activity,比如執行了onStop,優先級最低
當系統內存不足的時候,系統就會按照上述優先級去殺死目標Activity所在的進程,並且在後續通過onSaveInstanceState和onRestoreInstanceState來存儲和恢復數據,如果一個進程中沒有四大組件在執行,那麼這個進程將很快被系統殺死,因此,一些後台工作不適合脫離四大組建而獨立運行在後台中,這樣進程很容易就被殺死了,比較好的方法就是將後台工作放在Service中從而保證了進程有一定的喲徐愛你集,這樣就不會輕易的被殺死
上面分析了系統的數據存儲和恢復機制,我們知道,當系統配置發生改變後,Activity會被重新創建,那我們有沒有什麼辦法不重新創建尼?答案是有的,接下來我們來分析一下這個問題,系統配置中有很多內容,如果當某項內容發生改變後,我們不想系統重新創建,就可以給configChangs屬性加上orientation這個值
android:configChanges="orientation"
如果想指定多個值的話可以用“|”連接起來
mcc:The IMSI mobile country code (MCC) has changed — a SIM has been detected and updated the MCC.
IMSI(國際移動用戶識別碼)發生改變,檢測到SIM卡,或者更新MCC
mnc:The IMSI mobile network code (MNC) has changed — a SIM has been detected and updated the MNC.
IMSI網絡發生改變,檢測到SIM卡,或者更新MCC其中mcc和mnc理論上不可能發生變化
locale:The locale has changed — the user has selected a new language that text should be displayed in.
語言發生改變,用戶選擇了一個新的語言,文字應該重新顯示
touchscreen:The touchscreen has changed. (This should never normally happen.)
觸摸屏發生改變,這通常是不應該發生的
keyboard:The keyboard type has changed — for example, the user has plugged in an external keyboard.
鍵盤類型發生改變,例如,用戶使用了外部鍵盤
keyboardHidden:The keyboard accessibility has changed — for example, the user has revealed the hardware keyboard.
鍵盤發生改變,例如,用戶使用了硬件鍵盤
navigation:The navigation type (trackball/dpad) has changed. (This should never normally happen.)
導航發生改變,(這通常不應該發生) 舉例:連接藍牙鍵盤,連接後確實導致了navigation的類型發生變化。因為連接藍牙鍵盤後,我可以使用方向鍵來navigate了
screenLayout:The screen layout has changed — this might be caused by a different display being activated.
屏幕的布局發生改變,這可能導致激活不同的顯示
ontScale:The font scaling factor has changed — the user has selected a new global font size.
全局字體大小縮放發生改變
orientation:The screen orientation has changed — that is, the user has rotated the device.設備旋轉,橫向顯示和豎向顯示模式切換。
screenSize: 屏幕大小改變了
smallestScreenSize: 屏幕的物理大小改變了,如:連接到一個外部的屏幕上
4.2增加了一個layoutDirection屬性,當改變語言設置後,該屬性也會成newConfig中的一個mask位。所以ActivityManagerService(實際在ActivityStack)在決定是否重啟Activity的時候總是判斷為重啟。
需要在android:configChanges 中同時添加locale和layoutDirection。
在不退出應用的情況下切換到Settings裡切換語言,發現該Activity還是重啟了。
從上面的屬性中我們可以知道,如果我們沒有在Activity的configChanges中設備屬性的話,當系統發生改變後就會導致Activity重新被創建,上面表格中的項目很多,但是我們常用的只有locale,orientation,keyboardHidden這三個選項,其他用的還是比較少的,這裡設置之後顯示的效果我就不演示了
Android的啟動模式,是很有用的,對於Activity的棧的處理,也是極其講究的,所以你一定要清除他的標志位和啟動模式
首先說一下Activity為什麼需要啟動模式,我們知道,在默認的情況下,當我們多次啟動同一個Activity的時候,系統會創建多個實例並把他們一一放入任務棧中,當我們點擊back鍵的時候會發現這些Activity會一一回退,任務棧是一種先進先出的棧結構,這個好理解, 每按一次back鍵就有一個activity退出棧,知道棧空為止,當這個棧為空的時候,系統就會回收這個任務棧,關於任務棧的系統工作原理,這裡我們暫且不說,在後續章節也會介紹任務棧,知道了Activity的啟動模式,我們可發現一個問題,:多次啟動同一個Activity會創建多個實例,這樣是不是很逗,Activity在設計的時候不可能不考慮到這個問題,所以他提供了啟動模式來修改系統的默認行為,目前有四種啟動模式
standard singleTop singleTask singleInstance
我們先來把這幾種啟動模式都給介紹完
standard:標准模式,這也是系統的默認模式,每次啟動一個Activity都會重新創建一個實例,是否這個實例已經存在,被創建的實例的生命周期符合典型情況下Activity的生命周期,如上述:onCreate(),onStart();onResume()都會被調用,這是一種典型的多實例實現,一個任務棧都可以有多個實例,每個實例都可以屬於不同的任務棧,在這種模式下,誰啟動了這個Activity,那麼這個Activity就運行在啟動它的Activity所在的棧內,比如Activity A啟動了Activity B(B是標准模式),那麼B就會進入到A所在的棧內,不知道讀者有沒有注意到,當我們用ApplicationContext去啟動standard模式的Activity的時候就會報錯:
E/AndroidRuntime(674): android.util.androidruntiomException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_TASK flag . Is this really what are want?
相信讀者對這句話不會陌生,這是因為我們的standard模式的Activity默認會進入啟動它的Activity所屬的任務棧中,但是由於非Activity類型的Context(如ApplicationContext)並沒有所謂的任務棧,所以這就有問題了,解決這個問題,就是待啟動Activity指定FLAG_ACTIVITY_TASK標記位,這樣啟動的時候就會為他創建一個新的任務棧,這個時候待啟動Activity實際上是以singleTask模式啟動的,讀者可以仔細體會
singleTop:棧頂復用模式,在這個模式下,如果新的Activity已經位於任務棧的棧頂,那麼此Activity不會被重新創建,同時他的onNewIntent方法會被調用,通過此方法的參數我們可以取出當前請求的信息,需要注意的是,這個Activity的onCreate,onStart不會被系統調用,因為他並沒有發生改變,如果新Activity已存在但不是在棧頂,那麼新Activity則會重新創建,舉個例子,假設現在棧內的情況為ABCD,其中ABCD為四個Activity,A位於棧底,D位於棧頂,這個時候假設要再啟動D,如果D的啟動模式為singleTop,那麼站棧內的情況仍然是ABCD,如果D的啟動模式是standard,那麼由於D會被重新創建,導致情況就是ABCDD
singTask:棧內復用模式,這是一種單實例模式,在這種模式下,只要Activity在一個棧內存在,那麼多次啟動此Activity都不會創建實例,和singTop一樣,系統也會回調其onNewIntent方法,具體一點,當一個具有singleTask模式的Activity請求啟動後,比如Activity A,系統首先會去尋找是否存在A想要的任務棧,如果不存在,就小紅心創建一個任務棧,然後創建A的實例把A放進棧中,如果存在A所需要的棧,這個時候就要看A是否在棧中有實例存在,如果實例存在,那麼系統就會把A調到棧頂並調用它的onNewIntent方法,如果實例不存在,就創建A的實例並且把A壓入棧中,舉幾個例子
比如目前任務棧S1中的情況為ABC,這個時候Activity D以singleTask模式請求啟動,其所需的任務棧為S2,由於S2和D的實例都不存在,所以系統會先創建任務棧S2,然後創建D的實例將其入棧到S2 另外一種情況,假設D所需的任務棧為S1,其他情況如如上面的一樣,那麼由於S1已經存在,所以系統會直接創建D的實例並將其引入到S1中 如果D所需要的任務棧為S1,並且當前任務棧S1的情況為ABCD,根據棧內復用的原則,此時D不會被重新創建,系統會把D切換到棧頂並且調用其oNnNewIntent方法,同時由於singleTask默認具有clearTop的效果,會導致棧內所有在D上面的Activity全部出棧,於是最終S1中的情況為AD,這一點比較特殊,在後面還會對此情況詳細的分析
通過上述的三個例子,讀者應該還是比較清晰的理解singTask的含義吧
singleInstance:單實例模式,這是一種加強的singleTask的模式,他除了具有singleTask的所有屬性之外,還加強了一點,那就是具有此模式下的Activity只能單獨的處於一個任務棧中,換句話說,比如Activity A是singleInstance模式,當A啟動的時候,系統會為創建創建一個新的任務棧,然後A獨立在這個任務棧中,由於棧內復用的特性,後續的請求均不會創建新的Activity,除非這個獨特的任務棧被系統銷毀了
上面介紹了幾種啟動模式,這裡需要指出一種情況,我們假設目前有兩個任務棧,前台任務棧的情況為AB,而後台任務棧的情況是CD,這裡假設CD的啟動模式都是singleTask,現在請求啟動D,那麼整個後台任務站都會被切換到前台,這個時候整個後退列表變成了ABCD,當用戶按back鍵的時候,列表中的Activity會一一出棧,如圖
如果不是請求D,而是請求C,那麼情況就不一樣了,如圖,具體原因我們在後續章節中分析
另外一個問題,在singkleTask啟動模式中,多次提到了某個Activity所需的任務棧,什麼是Activity所需的任務棧尼?這要從一個參數說起:TaskAffinity,可以翻譯成任務相關性,這個參數標示了一個Activity所需要的任務棧的名字默認情況下,所有的Activity所需要的任務棧的名字為應用的包名,當然,我們可以為每個Activity都單獨指定TaskAffinity,這個屬性值必須必須不能和包名相同,否則就相當於沒有指定,TaskAffinity屬性主要和singleTask啟動模式或者allowTaskReparenting屬性配合使用,在其他狀況下沒有意義,另外,任務棧分為前台任務棧和後台任務棧,後台任務棧中的Activity位於暫停狀態,用戶可以通過切換將後台任務棧再次調為前台
當TaskAffinity和singleTask啟動模式配對使用的時候,他是具有該模式Activity目前任務棧的名字,待啟動的Activity會運行在名字和TaskAffinity相同的任務棧中
當TaskAffinity和allowTaskReparentiing結合的時候,這種情況比較復雜,會產生特殊的效果,當一個應用A啟動了應用B的某一個Activity後,如果這個Activity會直接從應用A的任務棧轉移到應用B的任務棧中,這還是很抽象的,再具體點,比如現在有2個應用A和B,A啟動了B的一個Activity C ,然後按Home鍵回到桌面,然後再單擊B的桌面圖標,這個時候並不是啟動; B的主Activity,而是重新顯示了已經被應用A啟動的Activity C,或者說,C從A的任務棧轉移到了B的任務棧中,可以這麼理解,由於A啟動了C,這個時候C只能運行在A的任務棧中,但是C屬於B應用,正常情況下,他的TaskAffinity值肯定不可能和A的任務棧相同(因為包名不同),所以,當B啟動後,B會創建自己的任務棧,這個時候系統發現C原本所想要的任務棧已經被創建出來了,所以就把C從A的任務棧中轉移過來,這種情況讀者可以寫一個例子測試一下,這裡就不做演示了
如何給Activity指定啟動模式?有兩種方法,第一種是通過清單文件為Activity指定
另一種啟情況就是通過intent的標志位為Activity指定啟動模式
Intent intent = new Intent(); intent.setClass(this,SecondActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);
這兩種方式都可以為Activity指定啟動模式,但是二者還是有一些區別的,首先,優先級上,第二種比第一種高,當兩種同時存在的時候,以第二種為准,其次,上述兩種方式在限定范圍內有所不同,比如,第一種方式無法直接為Activity設置FLAG_ACTIVITY_CLEAR_TOP標識,而第二種方式無法指定singleInstance模式
關於Intent中為Activity指定的各種標記位,在下面的小節中會繼續說道,下面通過一個實例來體驗啟動模式的使用效果,還是前面的例子,我們把MainActivity的啟動模式設置成singleTask,然後重復啟動它,看看他是否會重復創建
//點擊事件 findViewById(R.id.btnTo).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(); intent.setClass(MainActivity.this,MainActivity.class); intent.putExtra("time", System.currentTimeMillis()); startActivity(intent); } });
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d(TAG, "onNewIntent , time =" + intent.getLongExtra("time", 0)); }
上述修改,我們做了如下操作,連續點擊三次按鈕啟動三次,算上原本的MainActivity實例,正常情況下,任務棧中應該有四個MainActiivty實例,但是我們為其指定了singTask模式,我們一起來看下有何不同
執行命令
adb shell dumpsys activity
ok,輸入日志
從上面的信息中不難看出,盡管啟動了四次MainActivity,但是他始終只有一個實例在任務棧中,從log中我們也可以看到
Activity的確沒有被創建,只是暫停了一下,然後調用了onNewIntent,接著又onResume又繼續了,現在我們去掉singTask,再來比對一下我們的操作,同樣是點擊三次按鈕,執行adb命令
我們能夠得到目前總共有2個任務棧,前台任務棧taskAffinity值為包名。裡面有四個Activity,後台是一個com.android.launcher,他裡面就一個桌面
從上面的導出信息可以看到,在任務棧中有四個MainActivity,這也就驗證了Activity的啟動模式的工作方式
上述四種啟動模式,standard和singleTop都比較好理解,singleInstance由於其特殊性也比較好理解,但是關於singleTask還有一種情況需要理解,比如我們剛才那張啟動D的圖,在Activity B中請求的不是D而是C,那情況該如何尼?這裡可以告訴讀者的是,任務列表變成了ABC,是不是很奇怪,Activity為什麼直接出棧了,我們用一個實例來說明情況
我們把SecondActivity和CodeActivity的啟動模式都改成了singleTask,並且把android:taskAffinity設置為com.liuguilin.activitysample,注意這個taskAffinity的值屬於字符串,切中間必須有包名分割符‘.’,然後我們做如下的動作,在MainActivity中單擊按鈕啟動SecondActivity,在SecondActivity中單擊按鈕啟動CodeActivity,在CodeActivity啟動MainActivity,最後在MainActivity中啟動SecondActivity,,現在按Back,你知道會回那個Activity嗎?答案是桌面,是不是有點摸不著頭腦,我們看圖理解一下
首先,我們來分析一下這個問題,我們知道,A的啟動模式是standard,按照規定,A的taskAffinity繼承的是application的taskAffinity,而application默認taskAffinity是包名,所以A的taskAffinity是包名,由於我們在XML中為B和C指定了taskAffinity和啟動模式,且有相同的taskAffinity,所以B和C是singleTask模式且有相同的taskAffinity,所以A啟動B的時候,按照singleTask的規則,這個時候需要為B重新創建一個任務棧了,B再啟動C,按照singleTask的規則,由於C所需要的任務棧已經被B給創建了,所以無需再創建新的任務棧,這個時候系統只是創建C的實例放進任務棧,接著C再啟動A,A是標准模式,所以系統會為他創建一個新的實例並將他加入到啟動的任務棧中,由於是C啟動了A,所以A會進入C的棧內並處於棧頂,這個時候已經有兩個任務棧了,接著A再啟動B,由於B是singleTask,B需要回到任務棧的棧頂,由於棧的模式為‘先進先出’,B想要回到棧頂,就只能是CA出棧,所以到這裡就很好理解,按back鍵,B就出棧了,然後這個任務棧就是空的,被系統回收了,這個時候就只能是回到後台任務棧把A顯示出來,注意這個A是後台任務棧的A,不是BC棧的A,接著再按back就回到了桌面,分析到這裡,我們就得到了一條結論,singtleTask模式的Activity切換到棧頂會到導致在他之上的棧內activity出棧,我們可以看下運行的結果
接著我們再實驗中再次驗證這個問題,我們采用dumpasys命令,看看輸出的是什麼?
可以看到在B任務棧中只剩下B了,其他的都出棧了,這個時候按back肯定就回收了,分析到這裡,我相信讀者對Activity的啟動模式有了很深入的了解吧,下面我們再來說下Activity的標志位
Activity的Flags有很多,這裡主要是分析一些常用的標記位,標記位的作用很多,有些標志位可以設置Activity的啟動模式,比如FLAG_ ACTIVITY _ NEW _ TASK,還有一些直接影響Activity的運行狀態,比如FLAG_ ACTIVITY_ CLEAR_ TOP,下面我們來說下一些常用的標記位,剩下的讀者可以去看下官方文檔,大部分的情況下,Activity不需要設置標記位,因此對於標記位理解即可,在使用標記位的時候,要注意有些標記位是系統內部使用的,應用不需要去設置這些以防出問題。
FLAG_ ACTIVITY_ NEW _ TASK
這個標志位的作用是為Activity指向‘singleTask’啟動模式,其效果和XML中指定該模式相同
FLAG_ ACTIVITY_ SINGLE _ TOP
這個標志位的作用是為Activity指向‘singleTop’啟動模式,其效果和XML中指定該模式相同
FLAG_ ACTIVITY_ CLEAR _ TOP
具有此標記位的Activity,當他啟動時,在同一個任務棧中所有位於他上面的Activity都要出棧,這個模式一般需要和FLAG_ ACTIVITY_ NEW _ TASK配合使用,在這種情況下,被啟動的Activity的實例如果已經存在,那麼系統就會調用它的onNewIntent,如果被啟動的Activity采用標准模式,那麼他連同他之上的Activity都要出棧,系統會創建新的Activity實例並放入棧頂
FLAG_ ACTIVITY_ EXCLUDE_ FROM _ RECENTS
具有此標記位的Activity,不會出現在歷史Activity的列表當中,當某種情況下我們不希望用戶通過歷史列表回到我們的Activity的時候就使用這個標記位了,他等同於在XML中指定Activity的屬性:
android:excludeFromRecents="true"
我們知道,啟動Activity分為兩種,顯示調用和隱式調用,二者的區別這裡就不多講了,顯示調用需要明確的指定被啟動對象的組件信息,包括包名和類名,而隱式意圖則不需要明確指定調用信息,原則上一個intent不應該即是顯式調用又是隱式調用,如果二者共存的話以顯式調用為主,顯式調用很簡單,這裡主要介紹隱式調用,隱式調用需要intent能夠匹配目標組件的IntentFilter中所設置的過濾信息,如果不匹配將無法啟動目標Activity,IntentFilter中的過濾信息有action,category,data,下面是一個過濾規則的實例:
為了匹配過濾列表,需要同時匹配過濾列表中的action,category,data信息,否則匹配失敗,一個過濾列表中的action,category,data可以有多個,所有的action,category,data分別構成不同類別,同一類型的信息共同約束當前類別的匹配過程,只有一個intent同時匹配action類別,category類別,data類別才算是匹配完成,只有完全匹配才能成功啟動目標Activity,另外一點,一個Activity鐘可以有多個intent-filter,一個intent只要能匹配一組intent-filter即可成功啟動Activity
下面詳細分析各種屬性的匹配規則
1.action的匹配規則
action是一個字符串,系統預定了一些action,同時我們也可以在應用中定義自己的action,action的匹配規則是intent中的action必須能夠和過濾規則中的action匹配,這裡說的匹配是指action的字符串值完全一樣,一個過濾規則中的可以有多個action,那麼只要intent中的action能夠和過濾規則匹配成功,針對上面的過濾規則,需要注意的是,intent如果沒有指定action,那麼匹配失敗,總結一下,action的匹配需求就是intent中的action存在且必和過濾規則一樣的action,這裡需要注意的是他和category匹配規則的不同,另外,action區分大小寫,大小寫不同的字符串匹配也會失敗
2.category的匹配規則
category是一個字符串,系統預定義了一些category,同時我們也可以在應用中定義自己的category。category的匹配規則和action不同,它要求Intent中如果含有category,那麼所有的category都必須和過濾規則中的其中一個category相同。換句話說,Intent如果出現了category,不管有幾個category,對於每個category來說,它必須是過濾規則中已經定義的category。當然,Intent中可以沒有category,如果沒有category的話,按照上面的描述,這個Intent仍然可以匹配成功。這裡要注意下它和action匹配過程的不同,action
是要求Intent中必須有一個action且必須能夠和過濾規則中的某個action相同,而category要求Intent可以沒有category,但是如果你一旦有category,不管有幾個,每個都要能和過濾規則中的任何一個category相同。為了匹配前面的過濾規則中的category,我們可出下面的Intent,intent.addcategory (“com.ryg.category.c”)或者Intent.addcategory (“com rcategory.d)亦或者不設category。為什麼不設置category也可以匹配呢?原因是系統在調用startActivity或者startActivityForResult的時候會默認為Intent加上“android.intent.category.DEFAULT”這個category,所以這個category就可以匹配前面的過濾規則中的第三個category。同時,為了我們的activity能夠接收隱式調用,就必須在intent-filter中指定“android intent categor.DEFAULT”這個category,原因剛才已經說明了。
3.data匹配規則
data的匹配規則和action有點類似,如果過濾規則中定義了data,那麼intent中必須也要定義可匹配的data,在介紹data的匹配規則之前,我們需要來了解一下data的結構,因為data稍微有點復雜
data由兩部分組成,mimeType和URI,前者是媒體類型,比如image/jpeg等,可以表示圖片等,而URI包含的數據可就多了,下面的URI的結構:
:// " /[ | | ]
這裡再給幾個實際的例子就好理解了
content://com.liuguilin.project:200/folder/subfolder/etc http://www.baidu.com:80/search/info
看了上面的兩個例子你是不是瞬間就明白了,沒錯,就是這麼簡單,不過下面還是要一一介紹含義的:
Scheme:URI的模式,比如http.file.content等,如果URI中沒有指定的scheme,那麼整個URI的其他參數無效,這也意味著URI無效 Host:URI的主機,比如www.google.com,如果host未指定,那麼整個URI中的其他參數無效,這也意味著URI無效 Port:URI中的端口號,比如80,不過需要指定上面兩個才有意義 Path、pathPattem 和 pathPrefix:這三個參數表述路徑信息,其中path表示完整的路徑信息:pathPattern也表示完整的路徑信息,但是它裡面可以包含通配符“ * ”,“ * ” 表示0個或多個任意字符,需要注意的是,由於正則表達式的規范,如果想表示真實的字符串,那麼“* ” 要寫成 “ \*”,“ \ ”要寫成“ \\ ”,pathPrefix表示路徑的前綴信息。
介紹完data的數據格式後,我們要說一下data的匹配規則了。前面說到,data的匹配規則和action類似,它也要求Intent中必須含有data數據,並且data數據能夠完全匹配過濾規則中的某一個datn.這裡的完全匹配是指過濾規則中出現的data部分也出現在了 Intent
中的data中。下面分情況說明。
(1) 如下過來規則
這種規則指定了所有類型為圖片,那麼intent中的mineType屬性必須為“image/*”才能匹配,這種情況下雖然過來規則沒有指定URI,但是卻有默認值,URI的默認值為content何file,也就是說,雖然沒有指定URI,但是Intent中的URI部分的scheme必須為content或者file才能匹配,這點事需要注意的,為了匹配一種的規則我們可以這樣寫:
intent.setDataAndType(Uri.parse("file://abc"),"image/png");
另外,如果要為Intent指定完整的data,必須調用setDataAndType方法,不能縣調用setData在調用setType,因為這兩個方法彼此會清除對方的值,這個看源碼就比較好理解了,比如setData:
public Intent setData(Uri data) { mData = data; mType = null; return this; }
可以發現,setData會把類型設置為null,同樣的,對方也是
(2)如下規律規則
這種規則指定了兩組data規則,且每個data都指出了完整的屬性值,既有URI又有類型,為了匹配類型二,我們這樣寫“
intent.setDataAndType(Uri.parse("http://abc"),"video/png");
或者
intent.setDataAndType(Uri.parse("http://abc"),"audio/png");
通過上面的實例,我們應該知道了data的匹配規則,關於data還有一些特殊的情況需要說明一下,這也是他和action不同的地方
... ...
到這裡我們已經把IntentFilter的過濾規則都講了一遍了,還記得本書前面給出的一個實例嗎?現在我們給出完整的intent匹配規則
Intent intent = new Intent(); intent.addCategory("com.liuguilin.category.c"); intent.setDataAndType(Uri.parse("file//abc"),"text/plain"); startActivity(intent);
還記得URI中的scheme中的默認值嗎?如果把上面的intent.setDataAndType(Uri.parse(“file//abc”),”text/plain”);這句改成intent.setDataAndType(Uri.parse(“http//abc”),”text/plain”);打開的actiivty就會報錯,提示無法找到Activity,另外一點,intent-filter的匹配規則對於服務和廣播也是同樣的道理,不過系統對於Service的建議是盡量使用顯式意圖來啟動服務。
最後,當我們通過隱式方式啟動一個Activity的時候,可以做一下判斷,看是否Activity能夠匹配我們的隱式Intent,如果不做判斷就有可能出現上述的錯誤了。判斷方法有兩種:采用PackageManager的resolveActivity方法或者Intent 的resolveActivity方法,
果它們找不到匹配的Activity就會返回null,我們通過判斷返回值就可以規避上述錯誤了,另外,PackageManager還提供了queryIntentActivities方法,這個方法和resolveActivity方法法不同的是:它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息,我們看一下queryIntentActivities和resolveActivity的方法原型:
public abstract ListqueryIntentActivities(Intent intent,int fladgs); public abstract ResolveInfo resolveActivity(Intent intent,int flags);
上述兩個方法的第一個參數比較好理解,第二個參數需要注意,我們要使用MATCH_ DEFAULT _ ONLY這個標記位,這個標記位的含義是僅僅匹配那些在intentfilter中聲明了 < category android-name=”android.intent.category DEFAULT”>這個category的 Activity。使用這個標記位的意義在於,只要上述兩個方法不返回null,那麼startActivity一定可以成功,如果不用這個標記位,就可以把intent-filter 中 category不含DEFAULT的那些Activity給匹配出來,從而導致startActivity可能失敗。因為不含有DEFAULT這個category的Activity是無法接收隱式Intent的。在action和 category中,有一類action和category比較重要,他們是:
這二者共同作用是用來標明這是一個入口Activity並且會出現在系統的應用列表中,少了任何一個都沒有實際意義,也無法出現在系統的應用列表中,也就是二者缺一不可,另外,針對 Service和BroadcastReceiver,PackageManager同樣提供了類似的方法去獲取成功匹配的組件信息。
好的,我們的第一章就寫到這裡了,不得不說這是一本好書,非常的詳細,也希望大家仔細的閱讀
對於從Eclipse遷移項目到Android Studio中添加.jar文件和.so文件無疑是一件很重要也是很頭疼的問題!在最新版本中,默認是自動打包libs下面的所有.
才沒有完結呢o( ̄︶ ̄)n 。大家好,這裡是番外篇。拜讀了愛哥的博客,又學到不少東西。愛哥曾經說過: 要站在巨人的丁丁上。 那麼今天,我們就站在愛哥的丁丁上來學習制作一款
最近寫博客的時間,都是在晚上圖書館學習回到宿捨後,大概是11點半開始寫,寫著寫著就1點多了,這還是我積累的比較充分了的情況下的,然後自己要看一遍再睡覺。第二天早上還要審稿
完全屬於自己的新聞展示平台,展示給大家,希望大家喜歡。一、新聞的數據庫的構建腳本代碼如下:(使用的mysql5.0 數據庫)SET SQL_MODE = NO_AUTO_