播報:在Android系統中為什么需要廣播機制?
在Android系統中,廣播(Broadcast)是在組件之間傳播數據(Intent)的一種機制;這些組件甚至是可以位于不同的進程中,這樣它就像Binder機制一樣,起到進程間通信的作用。
在Android系統中,為什么需要廣播機制呢?如果是兩個組件位于不同的進程當中,那么可以用Binder機制來實現,如果兩個組件是在同一個進程中,那么它們之間可以用來通信的方式就更多了,這樣看來,廣播機制似乎是多余的。
然而,廣播機制卻是不可替代的,它和Binder機制不一樣的地方在于,廣播的發送者和接收者事先是不需要知道對方的存在的,這樣帶來的好處便是,系統的各個組件可以松耦合地組織在一起,這樣系統就具有高度的可擴展性,容易與其它系統進行集成。
(相關資料圖)
使用廣播的兩個步驟: 1、廣播的接收者需要通過調用registerReceiver函數告訴系統,它對什么樣的廣播有興趣,即指定IntentFilter,并且向系統注冊廣播接收器,即指定BroadcastReceiver:
IntentFilter counterActionFilter = new IntentFilter(CounterService.BROADCAST_COUNTER_ACTION); registerReceiver(counterActionReceiver, counterActionFilter);
這里,指定感興趣的廣播就是CounterService.BROADCAST_COUNTER_ACTION了,而指定的廣播接收器就是counterActonReceiver,它是一個BroadcastReceiver類型的實例。
2、廣播的發送者通過調用sendBroadcast函數來發送一個指定的廣播,并且可以指定廣播的相關參數:
Intent intent = new Intent(BROADCAST_COUNTER_ACTION); intent.putExtra(COUNTER_VALUE, counter); sendBroadcast(intent)
這里,指定的廣播為CounterService.BROADCAST_COUNTER_ACTION,并且附帶的帶參數當前的計數器值counter。調用了sendBroadcast函數之后,所有注冊了CounterService.BROADCAST_COUNTER_ACTION廣播的接收者便可以收到這個廣播了。
在第1步中,廣播的接收者把廣播接收器注冊到ActivityManagerService中;在第2步中,廣播的發送者同樣是把廣播發送到ActivityManagerService中,由ActivityManagerService去查找注冊了這個廣播的接收者,然后把廣播分發給它們。
在第2步的分發的過程,其實就是把這個廣播轉換成一個消息,然后放入到接收器所在的線程消息隊列中去,最后就可以在消息循環中調用接收器的onReceive函數了。這里有一個要非常注意的地方是,由于ActivityManagerService把這個廣播放進接收器所在的線程消息隊列后,就返回了,它不關心這個消息什么時候會被處理,因此,對廣播的處理是異步的,即調用sendBroadcast時,這個函數不會等待這個廣播被處理完后才返回。
虛線上面Step 1到Step 4步是注冊廣播接收器的過程,其中Step 2通過LoadedApk.getReceiverDispatcher在LoadedApk內部創建了一個IIntentReceiver接口,并且傳遞給ActivityManagerService;
虛線下面的Step 5到Step 11是發送廣播的過程,在Step 8中,ActivityManagerService利用上面得到的IIntentReceiver遠程接口,調用LoadedApk.performReceiver接口,LoadedApk.performReceiver接口通過ActivityThread.H接口的post函數將這個廣播消息放入到ActivityThread的消息隊列中去,最后這個消息在LoadedApk的Args.run函數中處理,LoadedApk.Args.run函數接著調用MainActivity.BroadcastReceiver的onReceive函數來最終處理這個廣播。
注冊廣播接收器(registerReceiver)
在Android的廣播機制中,ActivityManagerService扮演著廣播中心的角色,負責系統中所有廣播的注冊和發布操作,因此,Android應用程序注冊廣播接收器的過程就把是廣播接收器注冊到ActivityManagerService的過程。Android應用程序是通過調用ContextWrapper類的registerReceiver函數來把廣播接收器BroadcastReceiver注冊到ActivityManagerService中去的,而ContextWrapper類本身又借助ContextImpl類來注冊廣播接收器。
在Android應用程序框架中,Activity和Service類都繼承了ContextWrapper類,因此,我們可以在Activity或者Service的子類中調用registerReceiver函數來注冊廣播接收器。
我們先來看一下MainActivity是如何調用registerReceiver函數來注冊廣播接收器的:
public class MainActivity extends Activity implements OnClickListener {...... @Override public void onResume() {super.onResume(); IntentFilter counterActionFilter = new IntentFilter(CounterService.BROADCAST_COUNTER_ACTION); registerReceiver(counterActionReceiver, counterActionFilter); } ...... }
Step 1. ContextWrapper.registerReceiver
public class ContextWrapper extends Context {Context mBase; ...... @Override public Intent registerReceiver( BroadcastReceiver receiver, IntentFilter filter) {return mBase.registerReceiver(receiver, filter); } ...... }
這里的成員變量mBase是一個ContextImpl實例。
Step 2. ContextImpl.registerReceiver
class ContextImpl extends Context {...... @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {return registerReceiver(receiver, filter, null, null); } @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) {return registerReceiverInternal(receiver, filter, broadcastPermission, scheduler, getOuterContext()); } private Intent registerReceiverInternal(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context) {IIntentReceiver rd = null; if (receiver != null) {if (mPackageInfo != null && context != null) {if (scheduler == null) {scheduler = mMainThread.getHandler(); } rd = mPackageInfo.getReceiverDispatcher( receiver, context, scheduler, mMainThread.getInstrumentation(), true); } else {...... } } try {return ActivityManagerNative.getDefault().registerReceiver( mMainThread.getApplicationThread(), rd, filter, broadcastPermission); } catch (RemoteException e) {return null; } } ...... }
通過兩個函數的中轉,最終就進入到ContextImpl.registerReceiverInternal這個函數來了。這里的成員變量mPackageInfo是一個LoadedApk實例,它是用來負責處理廣播的接收的。
參數broadcastPermission和scheduler都為null,而參數context是上面的函數通過調用函數getOuterContext得到的,這里它就是指向MainActivity了,因為MainActivity是繼承于Context類的,因此,這里用Context類型來引用。
由于條件mPackageInfo != null和context != null都成立,而且條件scheduler == null也成立,于是就調用mMainThread.getHandler來獲得一個Handler了,這個Hanlder是后面用來分發ActivityManagerService發送過的廣播用的。這里的成員變量mMainThread是一個ActivityThread實例。
Step 3. ActivityThread.getHandler
public final class ActivityThread {...... final H mH = new H(); private final class H extends Handler {...... public void handleMessage(Message msg) {...... switch (msg.what) {...... } ...... } ...... } ...... final Handler getHandler() {return mH; } ...... }
有了這個Handler之后,就可以分發消息給應用程序處理了。
再回到上一步的ContextImpl.registerReceiverInternal函數中,它通過mPackageInfo.getReceiverDispatcher函數獲得一個IIntentReceiver接口對象rd,這是一個Binder對象,接下來會把它傳給ActivityManagerService,ActivityManagerService在收到相應的廣播時,就是通過這個Binder對象來通知MainActivity來接收的。
Step 4. LoadedApk.getReceiverDispatcher
final class LoadedApk {...... public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r, Context context, Handler handler, Instrumentation instrumentation, boolean registered) {synchronized (mReceivers) {LoadedApk.ReceiverDispatcher rd = null; HashMapmap = null; if (registered) {map = mReceivers.get(context); if (map != null) {rd = map.get(r); } } if (rd == null) {rd = new ReceiverDispatcher(r, context, handler, instrumentation, registered); if (registered) {if (map == null) {map = new HashMap (); mReceivers.put(context, map); } map.put(r, rd); } } else {rd.validate(context, handler); } return rd.getIIntentReceiver(); } } ...... static final class ReceiverDispatcher {final static class InnerReceiver extends IIntentReceiver.Stub {final WeakReference mDispatcher; ...... InnerReceiver(LoadedApk.ReceiverDispatcher rd, boolean strong) {mDispatcher = new WeakReference (rd); ...... } ...... } ...... final IIntentReceiver.Stub mIIntentReceiver; final Handler mActivityThread; ...... ReceiverDispatcher(BroadcastReceiver receiver, Context context, Handler activityThread, Instrumentation instrumentation, boolean registered) {...... mIIntentReceiver = new InnerReceiver(this, !registered); mActivityThread = activityThread; ...... } ...... IIntentReceiver getIIntentReceiver() {return mIIntentReceiver; } } ...... }
在LoadedApk.getReceiverDispatcher函數中,首先看一下參數r是不是已經有相應的ReceiverDispatcher存在了,如果有,就直接返回了,否則就新建一個ReceiverDispatcher,并且以r為Key值保在一個HashMap中,而這個HashMap以Context,這里即為MainActivity為Key值保存在LoadedApk的成員變量mReceivers中,這樣,只要給定一個Activity和BroadcastReceiver,就可以查看LoadedApk里面是否已經存在相應的廣播接收發布器ReceiverDispatcher了。
在新建廣播接收發布器ReceiverDispatcher時,會在構造函數里面創建一個InnerReceiver實例,這是一個Binder對象,實現了IIntentReceiver接口,可以通過ReceiverDispatcher.getIIntentReceiver函數來獲得,獲得后就會把它傳給ActivityManagerService,以便接收廣播。
在ReceiverDispatcher類的構造函數中,還會把傳進來的Handle類型的參數activityThread保存下來,以便后面在分發廣播的時候使用。
現在,再回到ContextImpl.registerReceiverInternal函數,在獲得了IIntentReceiver類型的Binder對象后,就開始要把它注冊到ActivityManagerService中去了。
Step 5. ActivityManagerProxy.registerReceiver
class ActivityManagerProxy implements IActivityManager {...... public Intent registerReceiver(IApplicationThread caller, IIntentReceiver receiver, IntentFilter filter, String perm) throws RemoteException {Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); data.writeStrongBinder(receiver != null ? receiver.asBinder() : null); filter.writeToParcel(data, 0); data.writeString(perm); mRemote.transact(REGISTER_RECEIVER_TRANSACTION, data, reply, 0); reply.readException(); Intent intent = null; int haveIntent = reply.readInt(); if (haveIntent != 0) {intent = Intent.CREATOR.createFromParcel(reply); } reply.recycle(); data.recycle(); return intent; } ...... }
這個函數通過Binder驅動程序就進入到ActivityManagerService中的registerReceiver函數中去了。
Step 6. ActivityManagerService.registerReceiver
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... public Intent registerReceiver(IApplicationThread caller, IIntentReceiver receiver, IntentFilter filter, String permission) {synchronized(this) {ProcessRecord callerApp = null; if (caller != null) {callerApp = getRecordForAppLocked(caller); if (callerApp == null) {...... } } List allSticky = null; // Look for any matching sticky broadcasts... Iterator actions = filter.actionsIterator(); if (actions != null) {while (actions.hasNext()) {String action = (String)actions.next(); allSticky = getStickiesLocked(action, filter, allSticky); } } else {...... } // The first sticky in the list is returned directly back to // the client. Intent sticky = allSticky != null ? (Intent)allSticky.get(0) : null; ...... if (receiver == null) {return sticky; } ReceiverList rl = (ReceiverList)mRegisteredReceivers.get(receiver.asBinder()); if (rl == null) {rl = new ReceiverList(this, callerApp, Binder.getCallingPid(), Binder.getCallingUid(), receiver); if (rl.app != null) {rl.app.receivers.add(rl); } else {...... } mRegisteredReceivers.put(receiver.asBinder(), rl); } BroadcastFilter bf = new BroadcastFilter(filter, rl, permission); rl.add(bf); ...... mReceiverResolver.addFilter(bf); // Enqueue broadcasts for all existing stickies that match // this filter. if (allSticky != null) {...... } return sticky; } } ...... }
函數首先是獲得調用registerReceiver函數的應用程序進程記錄塊:
ProcessRecord callerApp = null; if (caller != null) {callerApp = getRecordForAppLocked(caller); if (callerApp == null) {...... } }
這里得到的便是應用程序Broadcast的進程記錄塊了,MainActivity就是在里面啟動起來的。
List allSticky = null; // Look for any matching sticky broadcasts... Iterator actions = filter.actionsIterator(); if (actions != null) {while (actions.hasNext()) {String action = (String)actions.next(); allSticky = getStickiesLocked(action, filter, allSticky); } } else {...... } // The first sticky in the list is returned directly back to // the client. Intent sticky = allSticky != null ? (Intent)allSticky.get(0) : null;
這里傳進來的filter只有一個action,就是前面描述的CounterService.BROADCAST_COUNTER_ACTION了,這里先通過getStickiesLocked函數查找一下有沒有對應的sticky intent列表存在。什么是Sticky Intent呢?我們在最后一次調用sendStickyBroadcast函數來發送某個Action類型的廣播時,系統會把代表這個廣播的Intent保存下來,這樣,后來調用registerReceiver來注冊相同Action類型的廣播接收器,就會得到這個最后發出的廣播。這就是為什么叫做Sticky Intent了,這個最后發出的廣播雖然被處理完了,但是仍然被粘住在ActivityManagerService中,以便下一個注冊相應Action類型的廣播接收器還能繼承處理。
這里,假設我們不使用sendStickyBroadcast來發送CounterService.BROADCAST_COUNTER_ACTION類型的廣播,于是,這里得到的allSticky和sticky都為null了。
繼續往下看,這里傳進來的receiver不為null,于是,繼續往下執行:
ReceiverList rl = (ReceiverList)mRegisteredReceivers.get(receiver.asBinder()); if (rl == null) {rl = new ReceiverList(this, callerApp, Binder.getCallingPid(), Binder.getCallingUid(), receiver); if (rl.app != null) {rl.app.receivers.add(rl); } else {...... } mRegisteredReceivers.put(receiver.asBinder(), rl); }
這里其實就是把廣播接收器receiver保存一個ReceiverList列表中,這個列表的宿主進程是rl.app,這里就是MainActivity所在的進程了,在ActivityManagerService中,用一個進程記錄塊來表示這個應用程序進程,它里面有一個列表receivers,專門用來保存這個進程注冊的廣播接收器。接著,又把這個ReceiverList列表以receiver為Key值保存在ActivityManagerService的成員變量mRegisteredReceivers中,這些都是為了方便在收到廣播時,快速找到對應的廣播接收器的。
再往下看:
BroadcastFilter bf = new BroadcastFilter(filter, rl, permission); rl.add(bf); ...... mReceiverResolver.addFilter(bf);
上面只是把廣播接收器receiver保存起來了,但是還沒有把它和filter關聯起來,這里就創建一個BroadcastFilter來把廣播接收器列表rl和filter關聯起來,然后保存在ActivityManagerService中的成員變量mReceiverResolver中去。
發送廣播(sendBroadcast)的過程分析
前面我們分析了Android應用程序注冊廣播接收器的過程,這個過程只完成了萬里長征的第一步,接下來它還要等待ActivityManagerService將廣播分發過來。
廣播的發送過程比廣播接收器的注冊過程要復雜得多了,不過這個過程仍然是以ActivityManagerService為中心。廣播的發送者將廣播發送到ActivityManagerService,ActivityManagerService接收到這個廣播以后,就會在自己的注冊中心查看有哪些廣播接收器訂閱了該廣播,然后把這個廣播逐一發送到這些廣播接收器中,但是ActivityManagerService并不等待廣播接收器處理這些廣播就返回了,因此,廣播的發送和處理是異步的。
在分析廣播的發送過程前,我們先來看一下廣播發送過程的序列圖,然后按照這個序圖中的步驟來一步一步分析整個過程。
Step 1. ContextWrapper.sendBroadcast
public class ContextWrapper extends Context {Context mBase; ...... @Override public void sendBroadcast(Intent intent) {mBase.sendBroadcast(intent); } ...... }
Step 2. ContextImpl.sendBroadcast
class ContextImpl extends Context {...... @Override public void sendBroadcast(Intent intent) {String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try {ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, false, false); } catch (RemoteException e) {} } ...... }
這里的resolvedType表示這個Intent的MIME類型,我們沒有設置這個Intent的MIME類型,因此,這里的resolvedType為null。接下來就調用ActivityManagerService的遠程接口ActivityManagerProxy把這個廣播發送給ActivityManagerService了。
Step 3. ActivityManagerProxy.broadcastIntent
class ActivityManagerProxy implements IActivityManager {...... public int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String requiredPermission, boolean serialized, boolean sticky) throws RemoteException {Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo != null ? resultTo.asBinder() : null); data.writeInt(resultCode); data.writeString(resultData); data.writeBundle(map); data.writeString(requiredPermission); data.writeInt(serialized ? 1 : 0); data.writeInt(sticky ? 1 : 0); mRemote.transact(BROADCAST_INTENT_TRANSACTION, data, reply, 0); reply.readException(); int res = reply.readInt(); reply.recycle(); data.recycle(); return res; } ...... }
Step 4. ActivityManagerService.broadcastIntent
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... public final int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String requiredPermission, boolean serialized, boolean sticky) {synchronized(this) {intent = verifyBroadcastLocked(intent); final ProcessRecord callerApp = getRecordForAppLocked(caller); final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); int res = broadcastIntentLocked(callerApp, callerApp != null ? callerApp.info.packageName : null, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermission, serialized, sticky, callingPid, callingUid); Binder.restoreCallingIdentity(origId); return res; } } ...... }
這里調用broadcastIntentLocked函數來進一步處理。
Step 5. ActivityManagerService.broadcastIntentLocked
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... private final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String requiredPermission, boolean ordered, boolean sticky, int callingPid, int callingUid) {intent = new Intent(intent); ...... // Figure out who all will receive this broadcast. List receivers = null; ListregisteredReceivers = null; try {if (intent.getComponent() != null) {...... } else {...... registeredReceivers = mReceiverResolver.queryIntent(intent, resolvedType, false); } } catch (RemoteException ex) {...... } final boolean replacePending = (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0; int NR = registeredReceivers != null ? registeredReceivers.size() : 0; if (!ordered && NR > 0) {// If we are not serializing this broadcast, then send the // registered receivers separately so they don"t wait for the // components to be launched. BroadcastRecord r = new BroadcastRecord(intent, callerApp, callerPackage, callingPid, callingUid, requiredPermission, registeredReceivers, resultTo, resultCode, resultData, map, ordered, sticky, false); ...... boolean replaced = false; if (replacePending) {for (int i=mParallelBroadcasts.size()-1; i>=0; i--) {if (intent.filterEquals(mParallelBroadcasts.get(i).intent)) {...... mParallelBroadcasts.set(i, r); replaced = true; break; } } } if (!replaced) {mParallelBroadcasts.add(r); scheduleBroadcastsLocked(); } registeredReceivers = null; NR = 0; } ...... } ...... }
這個函數首先是根據intent找出相應的廣播接收器:
// Figure out who all will receive this broadcast. List receivers = null; ListregisteredReceivers = null; try {if (intent.getComponent() != null) {...... } else {...... registeredReceivers = mReceiverResolver.queryIntent(intent, resolvedType, false); } } catch (RemoteException ex) {...... }
回憶一下前面過程分析中的Step 6(ActivityManagerService.registerReceiver)中,我們將一個filter類型為BROADCAST_COUNTER_ACTION類型的BroadcastFilter實例保存在了ActivityManagerService的成員變量mReceiverResolver中,這個BroadcastFilter實例包含了我們所注冊的廣播接收器,這里就通過mReceiverResolver.queryIntent函數將這個BroadcastFilter實例取回來。由于注冊一個廣播類型的接收器可能有多個,所以這里把所有符合條件的的BroadcastFilter實例放在一個List中,然后返回來。在我們這個場景中,這個List就只有一個BroadcastFilter實例了,就是MainActivity注冊的那個廣播接收器。
繼續往下看:
final boolean replacePending = (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
這里是查看一下這個intent的Intent.FLAG_RECEIVER_REPLACE_PENDING位有沒有設置,如果設置了的話,ActivityManagerService就會在當前的系統中查看有沒有相同的intent還未被處理,如果有的話,就有當前這個新的intent來替換舊的intent。這里,我們沒有設置intent的Intent.FLAG_RECEIVER_REPLACE_PENDING位,因此,這里的replacePending變量為false。
再接著往下看:
int NR = registeredReceivers != null ? registeredReceivers.size() : 0; if (!ordered && NR > 0) {// If we are not serializing this broadcast, then send the // registered receivers separately so they don"t wait for the // components to be launched. BroadcastRecord r = new BroadcastRecord(intent, callerApp, callerPackage, callingPid, callingUid, requiredPermission, registeredReceivers, resultTo, resultCode, resultData, map, ordered, sticky, false); ...... boolean replaced = false; if (replacePending) {for (int i=mParallelBroadcasts.size()-1; i>=0; i--) {if (intent.filterEquals(mParallelBroadcasts.get(i).intent)) {...... mParallelBroadcasts.set(i, r); replaced = true; break; } } } if (!replaced) {mParallelBroadcasts.add(r); scheduleBroadcastsLocked(); } registeredReceivers = null; NR = 0; }
前面我們說到,這里得到的列表registeredReceivers的大小為1,且傳進來的參數ordered為false,表示要將這個廣播發送給所有注冊了BROADCAST_COUNTER_ACTION類型廣播的接收器,因此,會執行下面的if語句。這個if語句首先創建一個廣播記錄塊BroadcastRecord,里面記錄了這個廣播是由誰發出的以及要發給誰等相關信息。由于前面得到的replacePending變量為false,因此,不會執行接下來的if語句,即不會檢查系統中是否有相同類型的未處理的廣播。
這樣,這里得到的replaced變量的值也為false,于是,就會把這個廣播記錄塊r放在ActivityManagerService的成員變量mParcelBroadcasts中,等待進一步處理;進一步處理的操作由函數scheduleBroadcastsLocked進行。
Step 6. ActivityManagerService.scheduleBroadcastsLocked
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... private final void scheduleBroadcastsLocked() {...... if (mBroadcastsScheduled) {return; } mHandler.sendEmptyMessage(BROADCAST_INTENT_MSG); mBroadcastsScheduled = true; } ...... }
這里的mBroadcastsScheduled表示ActivityManagerService當前是不是正在處理其它廣播,如果是的話,這里就先不處理直接返回了,保證所有廣播串行處理。
注意這里處理廣播的方式,它是通過消息循環來處理,每當ActivityManagerService接收到一個廣播時,它就把這個廣播放進自己的消息隊列去就完事了,根本不管這個廣播后續是處理的,因此,這里我們可以看出廣播的發送和處理是異步的。
這里的成員變量mHandler是一個在ActivityManagerService內部定義的Handler類變量,通過它的sendEmptyMessage函數把一個類型為BROADCAST_INTENT_MSG的空消息放進ActivityManagerService的消息隊列中去。這里的空消息是指這個消息除了有類型信息之外,沒有任何其它額外的信息,因為前面已經把要處理的廣播信息都保存在mParcelBroadcasts中了,等處理這個消息時,從mParcelBroadcasts就可以讀回相關的廣播信息了,因此,這里不需要把廣播信息再放在消息內容中。
Step 7. Handler.sendEmptyMessage
這個自定義的Handler類實現在frameworks/base/services/java/com/android/server/am/ActivityManagerService.java文件中,它是ActivityManagerService的內部類,調用了它的sendEmptyMessage函數來把一個消息放到消息隊列后,一會就會調用它的handleMessage函數來真正處理這個消息:
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... final Handler mHandler = new Handler() {public void handleMessage(Message msg) {switch (msg.what) {...... case BROADCAST_INTENT_MSG: {...... processNextBroadcast(true); } break; ...... } } } ...... }
這里又調用了ActivityManagerService的processNextBroadcast函數來處理下一個未處理的廣播。
Step 8. ActivityManagerService.processNextBroadcast
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... private final void processNextBroadcast(boolean fromMsg) {synchronized(this) {BroadcastRecord r; ...... if (fromMsg) {mBroadcastsScheduled = false; } // First, deliver any non-serialized broadcasts right away. while (mParallelBroadcasts.size() > 0) {r = mParallelBroadcasts.remove(0); ...... final int N = r.receivers.size(); ...... for (int i=0; iObject target = r.receivers.get(i); ...... deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false); } addBroadcastToHistoryLocked(r); ...... } ...... } } ...... }
這里傳進來的參數fromMsg為true,于是把mBroadcastScheduled重新設為false,這樣,下一個廣播就能進入到消息隊列中進行處理了。前面我們在Step 5中,把一個廣播記錄塊BroadcastRecord放在了mParallelBroadcasts中,因此,這里就把它取出來進行處理了。廣播記錄塊BroadcastRecord的receivers列表中包含了要接收這個廣播的目標列表,即前面我們注冊的廣播接收器,用BroadcastFilter來表示,這里while循環中的for循環就是把這個廣播發送給每一個訂閱了該廣播的接收器了,通過deliverToRegisteredReceiverLocked函數執行。
Step 9. ActivityManagerService.deliverToRegisteredReceiverLocked
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... private final void deliverToRegisteredReceiverLocked(BroadcastRecord r, BroadcastFilter filter, boolean ordered) {boolean skip = false; if (filter.requiredPermission != null) {...... } if (r.requiredPermission != null) {...... } if (!skip) {// If this is not being sent as an ordered broadcast, then we // don"t want to touch the fields that keep track of the current // state of ordered broadcasts. if (ordered) {...... } try {...... performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver, new Intent(r.intent), r.resultCode, r.resultData, r.resultExtras, r.ordered, r.initialSticky); ...... } catch (RemoteException e) {...... } } } ...... }
函數首先是檢查一下廣播發送和接收的權限,在我們分析的這個場景中,沒有設置權限,因此,這個權限檢查就跳過了,這里得到的skip為false,于是進入下面的if語句中。由于上面傳時來的ordered參數為false,因此,直接就調用performReceiveLocked函數來進一步執行廣播發送的操作了。
Step 10. ActivityManagerService.performReceiveLocked
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {...... static void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver, Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky) throws RemoteException {// Send the intent to the receiver asynchronously using one-way binder calls. if (app != null && app.thread != null) {// If we have an app thread, do the call through that so it is // correctly ordered with other one-way calls. app.thread.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras, ordered, sticky); } else {...... } } ...... }
注意,這里傳進來的參數app是注冊廣播接收器的Activity所在的進程記錄塊,在我們分析的這個場景中,由于是MainActivity調用registerReceiver函數來注冊這個廣播接收器的,因此,參數app所代表的ProcessRecord就是MainActivity所在的進程記錄塊了;
而參數receiver也是注冊廣播接收器時傳給ActivityManagerService的一個Binder對象,它的類型是IIntentReceiver。
MainActivity在注冊廣播接收器時,已經把自己的ProcessRecord記錄下來了,所以這里的參數app和app.thread均不為null,于是,ActivityManagerService就調用app.thread.scheduleRegisteredReceiver函數來把這個廣播分發給MainActivity了。這里的app.thread是一個Binder遠程對象,它的類型是ApplicationThreadProxy。
Step 11. ApplicationThreadProxy.scheduleRegisteredReceiver
class ApplicationThreadProxy implements IApplicationThread {...... public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, String dataStr, Bundle extras, boolean ordered, boolean sticky) throws RemoteException {Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(receiver.asBinder()); intent.writeToParcel(data, 0); data.writeInt(resultCode); data.writeString(dataStr); data.writeBundle(extras); data.writeInt(ordered ? 1 : 0); data.writeInt(sticky ? 1 : 0); mRemote.transact(SCHEDULE_REGISTERED_RECEIVER_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } ...... }
這里通過Binder驅動程序就進入到ApplicationThread.scheduleRegisteredReceiver函數去了。
Step 12. ApplicaitonThread.scheduleRegisteredReceiver
public final class ActivityThread {...... private final class ApplicationThread extends ApplicationThreadNative {...... // This function exists to make sure all receiver dispatching is // correctly ordered, since these are one-way calls and the binder driver // applies transaction ordering per object for such calls. public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, String dataStr, Bundle extras, boolean ordered, boolean sticky) throws RemoteException {receiver.performReceive(intent, resultCode, dataStr, extras, ordered, sticky); } ...... } ...... }
這里的receiver是在前面過程分析中的Step 4中創建的,它的具體類型是LoadedApk.ReceiverDispatcher.InnerReceiver,即定義在LoadedApk類的內部類ReceiverDispatcher里面的一個內部類InnerReceiver,這里調用它的performReceive函數。
Step 13. InnerReceiver.performReceive
final class LoadedApk {...... static final class ReceiverDispatcher {final static class InnerReceiver extends IIntentReceiver.Stub {...... public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky) {LoadedApk.ReceiverDispatcher rd = mDispatcher.get(); ...... if (rd != null) {rd.performReceive(intent, resultCode, data, extras, ordered, sticky); } else {...... } } } ...... } ...... }
這里,它只是簡單地調用ReceiverDispatcher的performReceive函數來進一步處理,這里的ReceiverDispatcher類是LoadedApk類里面的一個內部類。
Step 14. ReceiverDispatcher.performReceive
final class LoadedApk {...... static final class ReceiverDispatcher {...... public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky) {...... Args args = new Args(); args.mCurIntent = intent; args.mCurCode = resultCode; args.mCurData = data; args.mCurMap = extras; args.mCurOrdered = ordered; args.mCurSticky = sticky; if (!mActivityThread.post(args)) {...... } } ...... } ...... }
這里mActivityThread成員變量的類型為Handler,它是前面MainActivity注冊廣播接收器時,從ActivityThread取得的。
這里ReceiverDispatcher借助這個Handler,把這個廣播以消息的形式放到MainActivity所在的這個ActivityThread的消息隊列中去,因此,ReceiverDispatcher不等這個廣播被MainActivity處理就返回了,這里也體現了廣播的發送和處理是異步進行的。
注意這里處理消息的方式是通過Handler.post函數進行的,post函數的參數是Runnable類型的,這個消息最終會調用這個這個參數的run成員函數來處理。這里的Args類是LoadedApk類的內部類ReceiverDispatcher的一個內部類,它繼承于Runnable類,因此,可以作為mActivityThread.post的參數傳進去,代表這個廣播的intent也保存在這個Args實例中。
Step 15. Hanlder.post
這個函數定義在frameworks/base/core/java/android/os/Handler.java文件中,它的作用就是把消息放在消息隊列中,然后就返回了,這個消息最終會在傳進來的Runnable類型的參數的run成員函數中進行處理。
Step 16. Args.run
final class LoadedApk {...... static final class ReceiverDispatcher {...... final class Args implements Runnable {...... public void run() {BroadcastReceiver receiver = mReceiver; ...... Intent intent = mCurIntent; ...... try {ClassLoader cl = mReceiver.getClass().getClassLoader(); intent.setExtrasClassLoader(cl); if (mCurMap != null) {mCurMap.setClassLoader(cl); } receiver.setOrderedHint(true); receiver.setResult(mCurCode, mCurData, mCurMap); receiver.clearAbortBroadcast(); receiver.setOrderedHint(mCurOrdered); receiver.setInitialStickyHint(mCurSticky); receiver.onReceive(mContext, intent); } catch (Exception e) {...... } ...... } ...... } ...... } ...... }
這里的mReceiver是ReceiverDispatcher類的成員變量,它的類型是BroadcastReceiver,這里它就是MainActivity注冊廣播接收器時創建的BroadcastReceiver實例了。
有了這個ReceiverDispatcher實例之后,就可以調用它的onReceive函數把這個廣播分發給它處理了。
Step 17. BroadcastReceiver.onReceive
public class MainActivity extends Activity implements OnClickListener {...... private BroadcastReceiver counterActionReceiver = new BroadcastReceiver(){public void onReceive(Context context, Intent intent) {int counter = intent.getIntExtra(CounterService.COUNTER_VALUE, 0); String text = String.valueOf(counter); counterText.setText(text); Log.i(LOG_TAG, "Receive counter event"); } } ...... }
這樣,MainActivity里面的定義的BroadcastReceiver實例counterActionReceiver就收到這個廣播并進行處理了。
至此,Android應用程序發送廣播的過程就分析完成了。
最后,我們總結一下這個Android應用程序發送廣播的過程:
Step 1 - Step 7,計數器服務CounterService通過sendBroadcast把一個廣播通過Binder進程間通信機制發送給ActivityManagerService,ActivityManagerService根據這個廣播的Action類型找到相應的廣播接收器,然后把這個廣播放進自己的消息隊列中去,就完成第一階段對這個廣播的異步分發了;Step 8 - Step 15,ActivityManagerService在消息循環中處理這個廣播,并通過Binder進程間通信機制把這個廣播分發給注冊的廣播接收分發器ReceiverDispatcher,ReceiverDispatcher把這個廣播放進MainActivity所在的線程的消息隊列中去,就完成第二階段對這個廣播的異步分發了;Step 16 - Step 17, ReceiverDispatcher的內部類Args在MainActivity所在的線程消息循環中處理這個廣播,最終是將這個廣播分發給所注冊的BroadcastReceiver實例的onReceive函數進行處理。標簽:
相關推薦:
精彩放送:
- []天天安卓模擬器是什么?安卓模擬器電腦版下載
- []csv文件用什么打開?什么是CSV文件?
- []利好政策漸次落地 多地房貸利率繼續下行
- []當前最新:龐溟專欄丨因城施策、動態調整,更好滿足剛性住房需求
- []當前熱門:教育金規劃的主要原則是什么
- []熱點在線丨旅游險的個人責任險和旅行責任險的區別
- []天天最資訊丨濟南市醫療保險查詢方法
- []環球速遞!領取養老保險金的條件是什么
- []每日看點!濟寧醫療保險個人賬戶余額查詢方法
- []世界速訊:天津首個成規模M0新型產業用地項目濱?!嘘P村北塘灣產業園啟動
- []天天觀察:住宅銷售待回溫 龍湖搶先披露經營性收入234億元
- []焦點短訊!深港口岸1月8日起分階段有序恢復人員正常往來
- []全球焦點!廣州最大古村落保留地之一文沖幸福里歷史文化街區開街營業
- []資本月報 | 融資總額7年來首次跌破萬億,年末迎來配股潮(2022年12月)
- []剛剛!800億A股巨頭宣布:實控人由馬云變為無實控人!多家上市公司披露權益變動
- []世界熱點!印力集團與上海港城達成戰略合作 規劃打造上海臨港片區商業地標
- []今日快看!福州住房公積金中心:第一季度住房公積金最高可貸80萬
- []阿里巴巴:馬云將不再控制君瀚和君澳持有的螞蟻集團多數投票權
- []全球熱點評!羊絨世家沖刺深交所上市:不再新增加盟商,蔣慶云父子為實控人
- []建發股份擬以現金方式協議收購紅星美凱龍不超過30%股份
- []焦點快看:新湖中寶計劃減持不超過7505.15萬股已回購股份 占總股本0.87%
- []當前熱文:美康生物:2022年12月30日江西省醫保局公示肝功生化試劑帶量采購擬中選結果,降價幅度符合市場預期
- []微信版花唄怎么開通
- []老馬茶室 | 新年開門紅,接近壓力位宜減倉
- []全球今日訊!港股暴漲40%!后市行情持續性如何?基金經理最新解讀
- []熱點在線丨儲蓄卡和信用卡的區別
- []香港內地今日通關,香港赴內地機酒訂單提前三天大漲兩倍
- []瑞信:預計今年恒生指數、滬深300等均存在20%以上上漲空間
- []世界聚焦:近十年主動權益冠軍基金盤點:“冠軍魔咒”頻現 高收益率難保持
- []格瑞士馬來西亞4MW鋁合金光伏地面工程
- []天天微資訊!哪里的增量配電有錢賺?各地配電價格指導意見盤點:滇貴實操性強 廣東可指定套餐
- []世界即時看!光伏支架龍頭成為“失信被執行人”
- []【環球新視野】權威數據 | 2022年二季度全國新能源電力消納評估分析
- []]稅優識別碼在保單上怎么找到,一般是在右上角
- []每日速訊:網上退保如何辦理?
- []車險千萬不要提前買,提前10到30天比較好
- []沃森生物:股權轉讓前,江蘇沃森主要承擔公司4價流感病毒裂解疫苗的開發工作
- []車胎沒氣可以找保險公司嗎?
- []只買三者險不買車損險,當然可以
- []即時看!深圳10大最慘新盤,誰碰誰倒霉?
- []春運人群旅行距離達三年新高,航班、鐵路出行半徑大增四到七成
- []堅持用戶至上,擁抱新能源時代紅旗品牌數字化轉型看點十足
- []當前快播:信質集團:公司與BYD一直保持緊密合作關系,公司為其提供新能源定、轉子鐵芯及部分總成業務
- []世界時訊:環京樓市調查:元旦期間銷量環比增長,房價仍在“筑底”
- []華統股份:公司火腿產能可達年80萬只,目前向市場保持正常有序供應
- []快看:豪邁科技:公司燃氣輪機零部件業務客戶國外有三菱、西門子和GE,也已經與東方電氣和上海電氣開展合作
- []全球看熱訊:【金融頭條】38城房價連降三個月 首套房貸利率開啟動態調整
- []天天視點!英力股份:公司將利用現有產能生產接線盒、邊框、背板等光伏組件配套產品,但暫時還未生產
- []天天報道:濮陽惠成:公司未開展光刻膠業務,公司的產品主要應用于電氣絕緣、復合材料、OLED光電材料等多個領域
- []香港旅游業迎曙光;旅行社踏上復蘇之路 | 一周速覽
- []環球觀焦點:愛彼迎中國出境游流量激增;達美航空投資10億美元提供免費 WiFi | 大公司簡報
- []當前快播:邏輯更正的新線索出現!“春季”行情已經發動
- []世界今日報丨百余家上市公司業績預告 超七成預喜
- []民航局:取消目的地為北京的國際客運航班從指定第一入境點入境
- []桂浩明:券商配股的悖論 券商為什么熱衷于配股?
- []世界報道:除夕火車票今日開售,熱門線路再現一票難求
- []全球熱點評!2023年首周5只基金按下“終止鍵” 近千只基金瀕臨清盤紅線
- []當前消息!專家:首套房貸利率下限更靈活,剛需、改善人群將受益
- []世界今亮點!消費全面復蘇!2023年北京零售市場預計新增67.5萬平方米
- []政策加碼力促首套住房消費需求釋放
- []“仰望”概念爆發,多股漲停!人氣龍頭股罕見“炸板”
- []私募看市:春季攻勢箭在弦上 看好復蘇預期下的龍頭股
- []焦點訊息:什么情況?基金暴賺買的人少 虧錢反而人氣旺 “北熱南冷”如何解?
- []國家電投浙江公司:分布式光伏電站數字化管控應用實踐及經驗分享
- []每日時訊!TOPCon Show Time!9.7晶科能源邀行業大咖共聚N型浦江夜
- []焦點速訊:氫能供需失調問題突出
- []當前資訊!強強聯合!天海防務&中舟風電攜手進軍海上光伏!
- []氫能標準丨《氫能汽車用燃料 液氫》全文發布
- []環球看熱訊:戶外運動保險和普通旅游險有哪些不同
- []環球播報:易方達科創50ETF單日成交創歷史新高 科創板今年一季度行情可期
- []世界今日訊!大病保險出險報案后怎么理賠?
- []今日要聞!英皇國際擬11.37億港元出售香港屯門13層高商廈
- []世界微資訊!被忽悠買錯保險怎么辦?
- []世界視點!龍湖2022年總合同銷售金額2016億元 實現經營性收入234億
- []環球滾動:星河灣旗下首座商業樓宇廣州“星河灣中心”啟用 建面逾12萬平
- []焦點速讀:不記名團體意外險的特點
- []仁恒置地2022年合約預售680.9億元 同比上升14.3%
- []重大疾病保險怎么理賠?
- []世界即時:伊戈爾:公司高頻電感、智能箱變等產品可以應用在儲能領域
- []全球熱點評!南京市秦淮區開展食品安全突發事件應急演練
- []盛視科技:1月5日公司高管賴時伍減持公司股份合計3000股
- []世界報道:大唐集團2022年合約銷售196億元 銷售面積197萬平米
- []【天天快播報】中原建業2022年在管項目合約銷售213.17億元
- []【獨家】陽光100中國2022年全年合約銷售12.05億元
- []速讀:深免集團中標深圳機場T3航站樓進出境免稅店項目運營
- []環球熱頭條丨央行:2022年第三季度我國支付體系運行平穩
- []信息:藍海華騰:1月5日公司高管徐學海、時仁帥減持公司股份合計33.22萬股
- []世界微動態丨中公教育:1月5日公司高管王振東減持公司股份合計130萬股
- []天天最新:康龍化成:1月5日公司高管鄭北、樓小強減持公司股份合計65萬股
- []每日訊息!王導:黃金1837繼續放空,目標1820
- []美國12月非農增幅下滑,薪資增幅大跌,黃金短線跳漲逾8美元
- []【天天聚看點】綠城集團2022年總合同銷售3003億元 代建銷售額約875億
- []報道:A股現抗病毒毛巾,抗病毒活性率超99.99%!公司稱已小批量發貨
- []天天關注:元旦后旅游復蘇現“二級火箭”:攜程春節游訂單連續5天日增30%
- []“致敬最美的你”——市委互聯網企業工委“踐行二十大精神 踔厲奮發新征程”活動圓滿舉行
- []玉龍股份:1月5日公司高管李振川、張鵬增持公司股份合計8000股
- []世界今日訊!融信中國單月合約銷售10.76億 2022年累計銷售578.73億元
- []天天新資訊:普元信息:1月4日至1月5日王蔥權減持公司股份合計12.13萬股
- []全球熱議:杰瑞股份:我們會積極與投資者保持溝通
- B站注冊資本增幅400%至5億 目前由陳睿全資持股
- 光源資本出任獨家財務顧問 沐曦集成電路10億元A輪融資宣告完成
- 巨輪智能2021年上半年營收11.24億元 期內研發費用投入增長19.05%
- 紅棗期貨尾盤拉升大漲近6% 目前紅棗市場總庫存約30萬噸
- 嘉銀金科發布2021年Q2財報 期內凈利潤達1.27億元同比增長208%
- 成都銀行2021上半年凈利33.89億元 期內實現營收同比增長17.27億元
- 汽車之家發布2021年第二季度業績 期內新能源汽車品牌收入增長238%
- 中信銀行上半年實現凈利潤290.31億元 期末不良貸款余額706.82億元
- 光伏概念掀起漲停潮交易價格創新高 全天成交額達1.29億元
- 上半年生物藥大增45% 關鍵財務指標好轉營收賬款持續下降
- 熱門看點:??诿鞴鈬H大廈1-5層資產第七次網拍 起拍價降至8603萬元
- 聚焦:赤峰黃金:1月4日李金千減持公司股份合計2.47萬股
- 正榮地產2022年全年實現合約銷售334.32億元
- 天天觀熱點:復星國際出售4家公司股權 總價67億元
- 中公教育:大股東魯忠芳女士的借款正在陸續到賬
- 南極電商:公司會根據各渠道發展情況積極調整業務規劃,以期用最便利的方式向消費者提供高性價比的產品
- 環球熱文:佳源國際第八次延長票據交換要約及同意征求屆滿期限
- 世界實時:國家郵政局:2022年12月中國快遞發展指數為327.2,同比提升5%
- 環球實時:中海商業簽約上海世博CK商辦、深圳星通大廈兩個輕資產項目
- 中海2022年合約銷售2947.62億元 收購了609萬平方米土地
- 速遞!飛豬升級春節服務保障:訂機票送防疫包、火車票無憂退、跟團游享五重保障
- 天天微速訊:產業類租戶成為寫字樓和商務園區需求增長點 倉儲物流供需均破歷史記錄
- 辛合學堂:玩轉短視頻,他將興趣變成能力
- 看熱訊:1月6日鹿山新材漲停分析:異質結電池HJT,光伏,OLED概念熱股
- 世界視訊!弘陽地產:2022累計合約銷售額為352.02億元
- 【全球新要聞】完美世界:公司下屬基金通過與環球影業的片單合作參與了《侏羅紀世界3:統治》的投資
- 全球報道:2023年房地產行業怎么走?多部門表態大力支持剛需購房
- 全球播報:1月6日東方盛虹漲停分析:光伏,PTA,滌綸概念熱股
- 當前時訊:樓市再迎重磅利好!首套房貸利率新政影響有多大?機構這么說丨火線解讀
- 快播:1月6日福萊特漲停分析:光伏,玻璃概念熱股
- 金地商置:2022累計合約銷售額617.73億元
- 全球今日報丨1月6日芯能科技漲停分析:光伏,BIPV概念,儲能概念熱股
- Halo 2023,攜手HALO光環家居一同探尋原生生活的100種可能
- 環球熱門:?洛陽老君山,被制造的頂流景區
- 焦點快報!一張圖:黃金原油匯股"樞紐點+多空占比"一覽(2023/01/06周五)
- 環球觀熱點:中信建投期貨1月6日交易策略
- 環球即時:現貨黃金交易策略:決戰非農!金價面臨大跌風險?
- 美原油交易策略:油價溫和反彈,后多頭機會來了?
- 天天快資訊丨1月6日匯市觀潮:歐元、英鎊和日元技術分析
- 2023春節車險優惠嗎,一般有優惠
- 世界今日訊!2023保險公司春節上班嗎,不上班
- 天天微速訊:2023春節公積金提取多久到賬,不能到賬
- 環球看點!2023公積金春節提現能到賬嗎,不能到賬
- 從讓理想飛揚,到夢想點亮未來,紅旗品牌吹響新能源號角
- 公積金有多少可以貸款,看當地的具體規定
- 法恩莎的2022:讓生活更加藝術
- 【當前熱聞】教師節的名人名言有哪些?分享一些教師節的名人名言?
- 多只ST股亮出“保殼”組合拳,退市風險能否解除?
- 當前熱門:北京自住商品房條件是什么?申請購買自住商品房的條件有哪些?
- 填報志愿的第一二志愿有什么區別?平行志愿是什么意思?
- 天天信息:中國什么時候成為常任理事國?中國成為常任理事國的意義?
- 杏子酒怎么釀制?杏子酒的釀制方法?
- 當前焦點!楊光的快樂生活砸車是第幾部第幾集?講述了什么劇情?
- 當前資訊!鐵杵磨針的作者是誰?鐵杵磨針的作者資料介紹?
- 超光速會發生什么?超光速源自于什么?
- 世界百事通!關于名勝古跡的對聯有哪些?
- 環球速讀:鐵生銹是物理變化還是化學變化?物理反應和化學反應有什么區別?
- 每日快訊!全球多個地區計劃運力全面復蘇,東南亞同比增幅超過50%
- 當前通訊!愛彼迎中國出境游流量激增,旅游業迎新年全面提振
- 八大券商主題策略:起底“鋰、電、光、儲”四大新能源賽道!2023年看好哪些標的?
- 告別桌面“盤絲洞” 倍思快充插線板給你整潔的桌面
- 變配電工程包括什么?
- 環球速讀:飲食安全小知識有哪些?
- 天天熱推薦:25tolife是什么意思?25tolife歌詞簡介?
- 世界簡訊:七臺河有哪些個大學?七臺河大學名單一覽?
- 今日快訊:超聲波導入儀是什么?超聲波導入儀的工作原理是什么?
- 全球最資訊丨炒黃金怎么開戶?開戶的基本流程是怎樣的?
- 最新消息:事業單位文秘專業知識考什么?
- 【環球報資訊】王者榮耀里的甄姬是哪個歷史人物?甄姬歷史資料介紹?
- 每日簡訊:南寧有哪些大學學校?
- 當前報道:粉玫瑰33朵代表什么意思?粉玫瑰的花語是什么?
- 旅游復蘇與分化:過萬別墅搶不到,三百元民宿無人問
- 【新視野】環球影城,又開始排長隊了
- 每日視訊:元旦三天海南免稅消費4.22億元,太古地產、中免布局零售項目欲借“東風”
- 2022年國際航空運輸市場回顧:復蘇路上的合作與發展
- 當前視訊!華泰證券:預計2023年光伏玻璃行業競爭或將更趨激烈
- 天天熱門:【東海期貨1月6日產業鏈日報】能化篇:成品油庫存數據良好,油價小幅反彈
- 張坤超級大反攻!兩月零3天 暴漲超40%!大批基金快速回血 有這個共同特點!
- 當前關注:深圳允許二手房交易可“帶押過戶”
- 紫鑫藥業:本公司生產的通竅鼻炎片屬于醫保乙類藥品
- 昊海生科:國家知識產權局已于2022年12月30日作出決定,認定愛博醫療的三項專利均存在有效性問題
- 時訊:餐飲與時尚業態收縮 北京零售市場2022年四季度租金下降近3%
- 【獨家】杭氧股份:公司未參與上海寶鋼氣體有限公司35%股權轉讓項目
- 中原內配:截止2022年12月31日,公司股東總戶數為59,471戶
- 專注空間更新及場景營造,京投發展TOD價值持續兌現
- 深圳推廣二手房“帶押過戶”模式
- 全球頭條:國內首個具備獨立運行能力的新能源儲能項目在內蒙古并網通電
- 實時:固態電池量產裝車可期 互聯網巨頭入局加速商業化進程
- 國泰君安:港股現時仍處于牛熊反轉初期 有望進入三級火箭行情
- 【全球獨家】外資投行建議“超配”中國資產 多只海外ETF強勢“吸金”
- 熱訊:湖州零碳電廠:建集中儲能46MW/468MWh,實現整村戶用儲能試點突破
- 天天微速訊:五一祝福圖片大全高清 五一祝福圖片大全
- 環球關注:DNF十三周年大飛空時代全地圖攻略(附下載)
- 股城網模擬炒股 哪個網站模擬股市?
- 天天速遞!你的曾經不想再去解釋是什么歌?厚顏無恥歌曲介紹
- 深圳住房公積金的提取條件是什么?深圳住房公積金提取流程
- 提高&優化CS反恐畫面效果/畫質(上)
- 孔維詩苑是誰?孔維詩苑的個人資料曝光
- 天天亮點!晉城一中2019中考錄取分數線是多少?多少分能上一中?
- 每日信息:安裝必備軟件——SharePointServer2010版本
- 樂果q9怎么樣?有哪些優勢?
- 每日熱門:全球首創!區塊鏈技術第一次用于塞拉利昂總統選舉
- 犯非法采礦罪!紫金礦業被判沒收違法所得4.6億元 并處罰金1500萬元
- 今亮點!Pythonbridge:10個Python開源項目
- 刪除數據的方法:GridView1_RowingEdit
- 三文魚被洗白后 病毒究竟如何進入北京新發地?
- 【時快訊】QQ空間怎么設置簽名?QQ空間的簽名檔在哪里設置?
- 劉信達痛批白敬亭:你怎么連嘴都不會親?
- 世界頭條:關于槨的讀音你知道多少?槨字詳情介紹
- 【環球速看料】外貿必備!常用搜索引擎查詢命令匯總