主頁 > 移動端開發 > Android--四大組件 (萬字詳細解讀!!!)

Android--四大組件 (萬字詳細解讀!!!)

2021-02-08 14:09:10 移動端開發

四大組件的生命周期

目錄

  • 四大組件的生命周期
    • Activity
      • 生命周期:這個結合它的各個方法來看
      • 方法間的區別
      • Activity的切換
      • Activity的四種啟動模式
      • 當按下Home鍵時,引起的變化
          • 記憶體資源不足,導致低優先級Activity被殺死
    • Service
      • 使用場景
      • 生命周期
      • 通信方式
      • 前臺服務
      • Service和Thread的區別
      • IntentService
      • 行程的優先級
    • BroadcastReceiver
      • 作用和地位
      • 使用方式
      • 靜態注冊和動態注冊
        • 靜態注冊
        • 動態注冊
        • 兩者區別與細節
      • BroadcastReceiver的分類
        • 無序廣播
        • 有序廣播
        • 本地廣播
        • sticky廣播
    • Content Provider
      • 地位和作用
      • 內容
        • URI
        • MIME資料型別
        • ContentProvider類
        • ContentResolver類
        • ContentUris類
        • UriMatcher類
        • ContentObserver類
      • 實體
        • DBHelper.java用于創建資料庫
        • 自定義ContentProvider類
        • 行程內的通信注冊
        • 行程內訪問ContentProvider的資料
        • 行程間的通信注冊
        • 行程二的ContentProvider
      • 優點
    • 作業程序
      • Activity的作業程序
      • Service的作業程序
        • 1. Service的啟動程序
        • 2. Service的系結程序
      • BroadcastReceiver的作業程序
        • 1. 廣播的注冊程序
        • 2. 廣播的發送和接收程序
      • ContentProvider的作業機制

Activity

用于表現功能,

生命周期:這個結合它的各個方法來看

方法名稱作用描述
onCreate()創建Activity,對Activity做一些初始化作業是生命周期的第一個方法,可以在這個方法里面加載布局、初始化控制元件等,此時Activity還在后臺,不可見,
onStart()啟動Activity,也可以將初始化作業放在這里面執行生命周期的第二個方法,此時Activity可見,但是沒有出現在前臺,所以用戶是看不見的
onResume()繼續重新開始這個Activity,可以打開獨占設備此時Activity已經在出現在前臺并且可見了
onPause()當前Activity暫停運行此時Activity依然在前臺可見,這個方法會在應用退出或者跳轉到另一個Activity時執行,當只有一個Activity的onPause方法執行完后,下一個Activity才會啟動,在Android中如果onPause方法在0.5秒內沒有執行完畢的話,就會強制關閉這個Activity,可以在這里快速重啟這個Activity,
onStop()停止這個Activity此時Activity已經不可見了,但是Activity物件還在記憶體中,沒有被銷毀,此方法中做的作業一般是一些資源的回收作業
onDestroy()銷毀這個Activity不可見,我們可以將還沒釋放的資源釋放,以及進行一些回收作業,
onRestart()重啟ActivityActivity在這時可見,當用戶按Home鍵切換到桌面后又切回來或者從后一個Activity切回前一個Activity就會觸發這個方法,這里一般不做什么操作,

方法間的區別

  • onCreate和onStart的區別
    • 可見與不可見
    • 執行次數
    • 能在onStart里面做的是不一定能在onCreate里面做,但是能在onCreate里面做的一定可以在onStart里面做(比如影片的初始化只能在onStart里面做)
  • onStart和onResume的區別
    • 是否在前臺
    • 職責不同,前者一般用于初始化,后者一般用于獨占設備的操作
  • onPause和onStop的區別
    • 是否可見
    • 記憶體不足可能不會執行onStop方法,所以程式狀態的保存、獨占設備和影片的關閉、以及一些資料的保存最好在onPause中進行,但要注意不能太耗時(0.5m)

Activity的切換

正常次序是(A)onPause→(B)onCreate→(B)onStart→(B)onResume→(A)onStop

可以發現A在B啟動完成后才啟動的onStop方法,這里有兩個原因:

  1. onPause方法會釋放掉這個Activity的很多占用的系統資源,為了保證切換的流暢性沒有必要再多等一個階段
  2. 如果用戶在切換的途中快速的切回原Activity就會直接呼叫onResume方法,會快很多,

Activity的四種啟動模式

  • Standard:標準的啟動模式,如果需要啟動一個activity就會創建該activity的實體,也是activity的默認啟動模式,
  • SingeTop:如果啟動的activity已經位于堆疊頂,那么就不會重新創建一個新的activity實體,而是復用位于堆疊頂的activity實體物件,如果不位于堆疊頂仍舊會重新創建activity的實體物件,
  • SingleTask:設定了singleTask啟動模式的activity在啟動時,如果位于activity堆疊中,就會復用該activity,這樣的話,在該實體之上的所有activity都依次進行出堆疊操作,即執行對應的onDestroy()方法,直到當前要啟動的activity位于堆疊頂,一般應用在網頁的圖集,一鍵退出當前的應用程式,
  • singleInstance:如果使用singleInstance啟動模式的activity在啟動的時候會復用已經存在的activity實體,不管這個activity的實體是位于哪一個應用當中,都會共享已經啟動的activity的實體物件,使用了singlestance的啟動模式的activity會單獨的開啟一個共享堆疊,這個堆疊中只存在當前的activity實體物件,
  1. Activity所需的任務堆疊

TaskAffinity:任務相關性,標識了一個Activity所需要的任務堆疊的名字,默認情況下是包名,必須和singleTask啟動模式或者allowTaskReparenting屬性配對使用,否則沒有意義,

任務堆疊分為前臺任務堆疊和后臺任務堆疊,后臺任務堆疊中的Activity處于暫停狀態,用戶可以通過切換將后臺任務堆疊調到前臺,

TaskAffinity和allowTaskReparenting結合使用:現在有2個應用A和B,A啟動了B的Activity B1,然后按Home鍵回到桌面,然后單擊桌面圖示啟動B,此時并不是啟動B的主Activity,而是重新顯示了Activity B1或者說,B1從A的任務堆疊轉移到了B的任務堆疊,可以這么理解,由于A啟動了B1,這個B1只能運行在A所在的任務堆疊中,但是B1是屬于B應用的,正常情況下,它的TaskAffinity應該是B的包名,所以,B被啟動之后,B會創建自己的任務堆疊,此時系統發現B1原本想要的任務堆疊已經有了,就會把B1從A的任務堆疊中轉移過,

  1. 指定啟動模式的兩種方式

第一種:通過AndroidMenifest.xml指定android:launchMode="singTask"
第二種:通過Intent的addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

Activity的Flags

Flags有很多,有些可以設定Activity的啟動模式,有些可以影響Activity的運行狀態,

FLAG_ACTIVITY_NEW_TASK為Activity指定“singleTask”啟動模式
FLAG_ACTIVITY_CLEAR_TOP具有此標記的Activity在啟動時,同一個任務堆疊中位于它上面的都要出堆疊,與singleTask一起使用,若實體已經存在,會呼叫newIntent方法;若被啟動的Activity是standard,則它自己也會出堆疊,然后重新創建一個新的Activity實體入堆疊,
FLAG_ACTIVITY_SINGLE_TOP為Activity指定“singleTop”啟動模式
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS等價于android:excludeFromRecents="true",表明此Activity不會出現在歷史Activity串列中

當按下Home鍵時,引起的變化

不管是按下home鍵還是查看任務串列或者是橫屏切換到豎屏,都屬于例外情況,二這些例外情況都會導致Activity有不同的生命周期,

  • Activity例外終止的情況下

    以豎屏切換成橫屏為例:

    這些例外情況是由于系統配置發生改變導致的Activity銷毀,所以onPause->onStop->onDestroy還是會走一遍,但是在onStop之前會呼叫onSaveIntenceState保存當前的Activity狀態,當切到橫屏后Activity會在onStart之后呼叫onRestoreInstanceState重建,并把onSaveIntanceState中保存的Bundle物件同時傳遞給onRestoreInstanceState和onCreate,

    當然,這種情況的發生也可以不用重新創建Activity,在Activity的配置中,配置屬性android:configChanges="orientation|screenSize",可以在螢屏旋轉的時候,不重新創建Activity,取而代之的是呼叫Activity的onConfigurationChanged方法

    其他可配置專案如下:

    專案含義
    locale一般指切換了系統語言
    keboardHidden鍵盤的可訪問性發生了變化
    orientation螢屏方向發生了變化
    screenSize螢屏尺寸資訊發生了變化,旋轉螢屏尺寸資訊就會發生變化,若編譯版本小于13,不會導致Activity的重啟;若大于13,則會導致Activity的重啟
  • 記憶體資源不足,導致低優先級Activity被殺死

    Activity的優先級:前臺Activity > 可見非前臺Activity > 后臺Activity

    這種Activity的銷毀也是通過onSaveIntanceState和onRestoreInstanceState來存盤和恢復資料,

Service

使用場景

Android的四大組件之一:

  • 長期運行在后臺沒有單獨的行程,運行在主執行緒中,沒有用戶界面,
  • 適合執行一些不需要顯示界面的后臺耗時操作,但耗時操作也需要放在子執行緒,
  • Service依賴于創建服務時所在的應用行程,當某個行程被殺掉以后,依賴于該行程的服務都會停止運行
  • 適用于下載網路資料、播放音樂、訪問檔案、資料庫等業務邏輯功能

生命周期

根據它不同的啟動方式有不同的生命周期

  • 啟動服務:startService()一旦啟動,Service將一直運行在后臺,即便啟動Service的組件已經被destory,但是,Service會在后臺執行單獨的操作,也并不會給啟動它的組件回傳結果,它的生命周期和啟動它的組件的生命周期無關

    方法名作用描述
    onCreate()創建服務由于是單例模式,所以無論啟動多少次,服務都只會創建唯一實體
    onStartCommand()啟動服務在重復啟動服務時,雖然不會重復創建實體,但是此方法會被重復呼叫
    onDestroy()銷毀服務服務是通過startService()啟動的,**stopService()**關閉的,所以在呼叫stopService()時,會回呼onDestroy

    **onStartCommand(Intent intent,int flag,int startId) **:這個方法的回傳值有三種START_STICKY:當Service因為記憶體不足而被系統殺掉時,一段時間后,記憶體如果再次空閑,系統將會重新創建此Service,創建成功后回呼此方法,但其中引數Intent為null,這個狀態下適合用于不執行命令、但無限期運行并等待作業的媒體播放器或類似服務,

    START_NOT_STICKY:和上面情況相同,不過就算記憶體空閑后,系統也不會重新創建這個Service,除非程式中再次呼叫startService,這樣可以避免在不必要時以及應用能夠輕松重啟所有未完成的作業時運行服務,

    START_REDELIVER_INTENT:和第一種情況相同,不過在重新創建的時候,會通過傳遞給服務的最后一個Intent呼叫onStartCommand(),任何掛起的Intent均以此傳遞,適合于主動執行應該立即恢復的作業(例如下載檔案)的服務

    引數flags有三個選值,表示啟動請求時是否有額外資料:

    0:0代表沒有

    START_FLAG_REDELIVERY:這個值代表回傳值為START_REDELIVER_INTENT,而且在上一次服務被殺死前會去呼叫stopSelf()方法停止服務

    START_FLAG_RETRY:該flag代表當onStartCommand呼叫后一直沒有回傳值時,會嘗試重新去呼叫onStartCommand(),

  • 系結服務:其他組件bindService()系結一個Service,通過系結方式啟動的Service是一個client-server結構,該Service可以與系結它的組件進行互動,一個系結的Service僅在僅有組件與其系結時才會運行,多個組件可與一個Service系結,所以當系結的組件如Activity出堆疊時,就會自動解綁,同時系結的Service也會停止并銷毀,

    方法名稱作用描述
    onCreate()創建服務作用同上面的onCreate
    onStartCommand()啟動服務在系結方式啟動服務時,這個方法不被呼叫
    onBind()系結服務回傳一個Binder物件,這個物件是自己寫的一個繼承Binder的類
    onUnbind()解綁服務在Activity中呼叫unBindService會回呼此方法,服務解綁后就會被Destroy

呼叫bindService或者是unBindService都會傳入引數ServiceConnection,所以要一個實作介面ServiceConnection的類:

/**
     * 創建類繼承ServiceConnection,用于解綁服務方法呼叫
     */
    public class MyServiceConnection implements ServiceConnection{
        //當客戶端正常連接這個服務時,成功系結到服務時呼叫該方法,注意IBinder引數物件
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            //如果成功系結,iBinder為MyService里面的IBinder物件
            Log.i("MainActivity","服務系結成功,記憶體地址為:"+iBinder.toString());
        }
        //當客戶端與服務失去連接時呼叫該方法
        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            //解綁
            Log.i("MainActivity","服務解綁成功");
        }
    }

通信方式

在Android中,服務的通信方式有兩種:本地服務通信遠程服務通信,使用這兩種方式進行通信時,必須保證服務是以系結的形式開啟的,否則無法進行通信和資料交換

  1. 本地服務通信

? 指的是應用程式內部的通信,首先需要創建一個Service類,該類會提供一個onBind()方法,onBind()方法的回傳值是一個IBinder物件,IBinder物件會作為引數傳遞給ServiceConnection類中的onServiceConnected(ComponentName name,IBinder service)方法,這樣訪問者就可用通過IBinder物件與Service進行通信,如下圖所示:

img

? 從上圖可以看出,服務在進行通信時實際使用的是IBinder物件,在ServiceConnection類中得到的IBinder物件,通過這個物件可以獲取到服務中自定義的方法,執行具體的操作,

  1. 遠程服務通信

? 在Android系統中,各個應用程式都運行在自己的行程中,如果想要完成不同行程之間的通信,就需要用到遠程服務通信,遠程服務通信時通過AIDL(Android Interface Definition Language)實作的,介面定義語言,語法格式簡單,與Java中定義介面類似,但存在差異如下:

AUDL定義介面的源代碼必須以.aidl結尾,

AIDL介面中用到的資料型別,除了基本資料型別及String、List、Map、CharSequence之外,其他型別全部都需要匯入到包,即使它們在同一個包中,

前臺服務

  • 服務在后臺運行,優先級比較低,當記憶體不足時可以會被系統回收,可以使用前臺服務,

  • 前臺服務會在狀態欄中顯示圖示,并且下拉狀態欄,可以在通知欄中看到詳細資訊,類似于通知

(API26開始使用新的方法startForegroundService()來啟動前臺服務,和startService一樣的效果

要求Service里有startForeground()方法,否則startForegroundService()啟動前臺服務會報ANR錯誤)

Service和Thread的區別

首先Service與Thread無關,只不過都有一個后臺的概念,

類別相同不同
Service執行異步操作運行在主執行緒,不依賴UI,行程在,Service就在,所有Activity都能與Service關聯獲得Binder實體并操作
Thread執行異步操作運行在作業執行緒,在一個Activity創建的子執行緒,另一個Activity無法操作,當Activity銷毀之后就無法在獲得之前的子執行緒實體

IntentService

繼承于Service的一個封裝類,一般用于離線下載,將執行緒任務按順序在后臺執行

行程的優先級

  • 前臺行程:某個Activity可見,獲得焦點

  • 可見行程:某個Activity可見,但是沒有焦點

  • 服務行程:有一個服務在后臺運行

  • 后臺行程:沒有任何服務,打開一個Activity然后最小化(容易被回收)

  • 空行程:沒有任何活動組件存在的行程(容易被回收)

BroadcastReceiver

作用和地位

Android四大組件之一,廣播接受者,應用于系統接收到短信、系統電量不足、APP開機自啟等,廣播的發送者是Broadcast,和接收者之間的傳遞資料是Intent,

使用方式

繼承BroadcastReceiver { }

實作抽象方法onReceive()

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();//獲取到收到的廣播的名稱
        Log.e("123", "收到的廣播的Action是:"+action);
    }
}

注冊BroadcastReceiver,

  • BroadcastReceiver運行在UI執行緒所以不能執行耗時操作
  • 執行耗時操作可以考慮通過Intent啟動一個Service
  • 監聽到就會創建實體,觸發onReceiver方法,執行完改方法,銷毀實體
  • 通過Intent激發BroadcastReceiver時,找不到也不會報錯

靜態注冊和動態注冊

靜態注冊

靜態注冊就是將Broadcast注冊在AndroidManifest.xml清單檔案中

例如:監聽手機打電話,如果有打電話的這個行為就發送廣播

<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
 <receiver android:name=".MyBroadcastReceiver">
      <intent-filter>
           <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
       </intent-filter>
</receiver>

動態注冊

在代碼中注冊廣播,比如下例:

監聽螢屏的點亮與關閉

public class MainActivity extends Activity {

    private MyBroadcastReceiver receiver;//MyBroadcastReceiver為繼承于BroadcastReceiver的類
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        registerMyReceiver();//在activity創建的時候進行注冊監聽
    }

    private void registerMyReceiver() {
        receiver = new MyBroadcastReceiver();
        IntentFilter filter = new IntentFilter();//創建IntentFilter物件
        filter.addAction(Intent.ACTION_SCREEN_OFF);//IntentFilter物件中添加要接收的關屏廣播
        filter.addAction(Intent.ACTION_SCREEN_ON);//添加點亮螢屏廣播
        registerReceiver(receiver, filter);
    }

    private void unRegisterMyReceiver(){
        if(receiver != null){
            unregisterReceiver(receiver);//反注冊廣播,也就是注銷廣播接收者,使其不起作用
        }
    }
}

兩者區別與細節

  • 靜態注冊依附于清單檔案,只要APP啟動過一次,就算關閉了APP(除非卸載)只要相應的廣播事件發生,系統就會遍歷所有的清單檔案,通知相應的廣播接受者接收廣播,缺點就是耗電、占記憶體,
    • 這個清單檔案的receiver節點可以添加android:exported=”false”屬性 ,這樣系統遍歷全部APP清單檔案的廣播接收者時不會對本receiver進行判斷及處理,
      這個值為FALSE表示不予其他APP相互動,
  • 動態注冊依賴于注冊他的組件,當APP關閉后,廣播就失效了,
  • 所以靜態注冊的廣播傳播速度要小于動態廣播
  • 有個原則(谷歌的源代碼規定):動態注冊的廣播優先級大于靜態廣播

注意
動態廣播最好在Activity的onResume()注冊、onPause()注銷,不銷毀會導致記憶體泄漏,(重復注冊、重復注銷也不允許),不在onCreate() & onDestory() 或 onStart() & onStop()注冊、注銷是因為:
當系統因為記憶體不足(優先級更高的應用需要記憶體)要回收Activity占用的資源時,Activity在執行完onPause()方法后就會被銷毀,有些生命周期方法onStop(),onDestory()就不會執行,當再回到此Activity時,是從onCreate方法開始執行,
假設我們將廣播的注銷放在onStop(),onDestory()方法里的話,有可能在Activity被銷毀后還未執行onStop(),onDestory()方法,即廣播仍還未注銷,從而導致記憶體泄露,但是,onPause()一定會被執行,從而保證了廣播在App死亡前一定會被注銷,從而防止記憶體泄露,(詳見Activity生命周期圖)

BroadcastReceiver的分類

無序廣播

當廣播發送以后與之相關的所有廣播接受者都會收到廣播,沒有先后順序,就比如一個通知檔案要下達到所有人,這個時候管理員在群里直接發了個通知,然后所有人就都可以收到這個通知了,也就是**sendBroadcast();**這個方法

public void sendCustomBroadcast(View view){
        Intent intent = new Intent("my.broadcast.message");//action是my.broadcast.message,清單檔案中的action與之一致方可收到廣播
        intent.putExtra("qian", "100");//廣播中攜帶的資料
        sendBroadcast(intent);//發送無序廣播
    } 

有序廣播

一種可以分先后廣播接收者順序的發送方式,廣播接收者的優先級(范圍在-1000~1000之間)越高,越先接收廣播,優先級高的廣播先收到廣播,收到廣播后可以修改廣播的內容,也可以攔截廣播不讓廣播向下傳遞,比如,學校發了一個通知給老師,然后老師下發給學生,最后學生才知道這個通知,老師可以將這個通知攔截下來不給學生,也可以修改這個通知再發給學生,

所以它有兩種發送方式:

  • 這里使用靜態注冊的方式,在清單中設定它的優先級

    <receiver android:name=".MainReceiver">
                <intent-filter android:priority="100">//設定優先級,為整數,越大優先級越高
                    <action android:name="my.broadcast.message"/>
                </intent-filter>
            </receiver>
            <receiver android:name=".MainReceiver2">
                <intent-filter   android:priority="200" >
                    <action android:name="my.broadcast.message"/>
                </intent-filter>
            </receiver>
    
    public void sendCustomBroadcast(View view){
            Intent intent = new Intent("my.broadcast.message");//action是my.broadcast.faqian,清單檔案中的action與之一致方可收到廣播
            intent.putExtra("qian", "100");//廣播中攜帶的資料 
             /**
             * sendOrderedBroadcast(Intent intent, String receiverPermission);
             */
            sendOrderedBroadcast(intent, null);//發送有序廣播
        }
    

    結果就是MainReceiver比MainReceiver2先接到通知

  • 使用sendOrderedBroadcast的另一個多載方法

    sendOrderedBroadcast( Intent intent,//封裝了action及其他資料 String receiverPermission, //廣播接收者需要的權限 BroadcastReceiver resultReceiver,//表示無論當廣播傳播結束還是攔截以后我任然會收到廣播 Handler scheduler, int initialCode, String initialData, Bundle initialExtras);

     public void sendCustomBroadcast(View view){
            Intent intent = new Intent("my.broadcast.message");//action是my.broadcast.message,清單檔案中的action與之一致方可收到廣播
    
            Bundle bundle = new Bundle();
            bundle.putString("qian","100");//廣播中攜帶的bundle資料 
            sendOrderedBroadcast(
            intent,
        null, //permission為null
        new MainReceiver2(), //這里的new MainReceiver2()為最終的廣播接收者,也就是說無論他曾經有沒有收到廣播都會再次收到廣播,
        null,
         666,//initCode
         "我是initialData",//initData
        bundle);//bundle
        //以上所有入參都會攜帶在廣播中,如何取出呢?
        }
    
    public class MainReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
    
            /***********獲取資料*************/
            int initCode = getResultCode();//獲取傳遞過來的initCode
            String initData = getResultData();//獲取傳遞過來的initData
            Bundle initBundle = getResultExtras(true);//獲取傳遞過來的Bundle
    
            Log.d("hui",  "ZhiXianReceiver = " +"initCode = "+initCode +" ,initdata = " +initData +" ,bundle = " +initBundle.getString("qian"));
    
        }
    }
    
    public class MainReceiver2 extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
    
            /***********獲取資料*************/
            int initCode = getResultCode();//獲取傳遞過來的initCode
            String initData = getResultData();//獲取傳遞過來的initData
            Bundle initBundle = getResultExtras(true);//獲取傳遞過來的Bundle
    
            Log.d("hui",  "ZhiFuReceiver = " +"initCode = "+initCode +" ,initdata = " +initData +" ,bundle = " +initBundle.getString("qian"));
        }
    }
    
    
    

    結果就是在方式一的基礎上MainReceiver2會收到兩次廣播

    可以在MainReceiver這種接收者中寫入abortBroadcast();*//攔截廣播,廣播被終止,以后不會有其他廣播接收者再收到廣播了,*這個方法,攔截廣播

    也可以在收到廣播后去修改廣播使用下面這幾個方法

    		setResultCode(3306);//修改initCode
    
            setResultData("MainReceiver修改了資料"); //修改initData
    
            //修改bundle資料
            Bundle bundle = new Bundle();
            bundle.putString("qian", "10");
            setResultExtras(bundle);
    

本地廣播

上面兩種發出的廣播手機里面所有的APP都能收到,本地廣播是區域的廣播基于本程式的廣播,其他的程式無法收到這個廣播,這種廣播是安全的,外界不會干擾他,廣播也不會被其他行程所收到,就好像老師收到通知后單獨的發給了某一個學生,所以這個通知只被老師和學生知道.

    /**
     * 本地廣播接收者進行注冊,必須在代碼中注冊,清單檔案注冊是無效的
     */
    public void registerMyAPPReceiver(View view) {
        //創建廣播接收者
        MyBroadCastReceiver myBroadCastReceiver = new MyBroadCastReceiver();
        MyBroadcastReceiver2 myBroadCastReceiver2 = new MyBroadcastReceiver2();
        //封裝要接收的廣播型別
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("my.broadcast.Message2");
        //拿到LocalBroadcastManager物件,對固定的Receiver進行注冊,成為本地廣播接收者
        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(MainActivity.this);
        localBroadcastManager.registerReceiver( myBroadCastReceiver, intentFilter);
        localBroadcastManager.registerReceiver(myBroadCastReceiver2, intentFilter);
    }
  • registerReceiver注冊一個廣播接收者可以多次執行,比如:我把LocalBroadcastManager.registerReceiver( myBroadCastReceiver, intentFilter);寫兩遍,那么myBroadCastReceiver的onReceiver會被呼叫兩次,不建議這樣寫,
  • 本地廣播不能攔截
  • registerReceiver對應的還有unregisterReceiver(receiver

sticky廣播

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-A5rtei6f-1612606912809)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210203210241851.png)]

Content Provider

地位和作用

Android四大組件之一,但使用的頻率沒有其他的三種組件高,他主要是實作兩個行程(兩個應用)間的資料共享和互動,比如手機QQ要獲取手機的通訊錄的資料…

原理屬于是Android的Builder機制…

內容

說到ContentProvider那么和他相關的幾個東西就得先擺出來了…

URI

這個全稱叫做Uniform Resource Identifier統一資源識別符號,如名稱所示,就是識別符號,標識的內容就是ContentProvider和其中的資料,這個也用在過Activity的隱式啟動上,格式如下:

[scheme:][//host:port][path][?query]

URI有分為預置的和自定義的,預置的一般有通訊錄、日程表等資料,自定義的話例如下面這個:

Uri uri = Uri.parse("content://com.carson.provider/User/1")

標識的資源就是名為 com.carson.providerContentProvider 中表名 為User 中的 id為1的資料,

并且URI模式存在匹配通配符,

MIME資料型別

這個主要用于指定某個擴展名的檔案用某種應用程式來打開,比如指定.html檔案由text程式打開,

這個型別一般是在ContentProvider里面的getType(Uri uri)里面回傳得到,

MIME型別資料由兩部分組成:型別 + 子型別

 // 形式1:單條記錄  
vnd.android.cursor.item/自定義
// 形式2:多條記錄(集合)
vnd.android.cursor.dir/自定義 

// 注:
  // 1. vnd:表示父型別和子型別具有非標準的、特定的形式,
  // 2. 父型別已固定好(即不能更改),只能區別是單潭訓是多條記錄
  // 3. 子型別可自定義

ContentProvider類

主要以表格的形式組織資料,它的核心方法有四個:增、刪、改、查

<-- 4個核心方法 -->
  public Uri insert(Uri uri, ContentValues values) 
  // 外部行程向 ContentProvider 中添加資料

  public int delete(Uri uri, String selection, String[] selectionArgs) 
  // 外部行程 洗掉 ContentProvider 中的資料

  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
  // 外部行程更新 ContentProvider 中的資料

  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,  String sortOrder)  
  // 外部應用 獲取 ContentProvider 中的資料

// 注:
  // 1. 上述4個方法由外部行程回呼,并運行在ContentProvider行程的Binder執行緒池中(不是主執行緒)
 // 2. 存在多執行緒并發訪問,需要實作執行緒同步
   // a. 若ContentProvider的資料存盤方式是使用SQLite & 一個,則不需要,因為SQLite內部實作好了執行緒同步,若是多個SQLite則需要,因為SQL物件之間無法進行執行緒同步
  // b. 若ContentProvider的資料存盤方式是記憶體,則需要自己實作執行緒同步

<-- 2個其他方法 -->
public boolean onCreate() 
// ContentProvider創建后 或 打開系統后其它行程第一次訪問該ContentProvider時 由系統進行呼叫
// 注:運行在ContentProvider行程的主執行緒,故不能做耗時操作

public String getType(Uri uri)
// 得到資料型別,即回傳當前 Url 所代表資料的MIME型別

ContentResolver類

ContentProvider不直接與外部行程互動,而是通過ContentResolver,一款應用要和多個ContentProvider互動,如果不用ContentResolver進行抽象,那么可能在互動時要考慮是SQLite的資料庫還是MongoDB資料庫,這樣的話操作成本高、難度大,

ContentResolver也提供了名稱相同的增、刪、改、查四個方法,使用方法如下:

// 使用ContentResolver前,需要先獲取ContentResolver
// 可通過在所有繼承Context的類中 通過呼叫getContentResolver()來獲得ContentResolver
ContentResolver resolver =  getContentResolver(); 

// 設定ContentProvider的URI
Uri uri = Uri.parse("content://cn.scu.myprovider/user"); 
 
// 根據URI 操作 ContentProvider中的資料
// 此處是獲取ContentProvider中 user表的所有記錄 
Cursor cursor = resolver.query(uri, null, null, null, "userid desc"); 

ContentUris類

這個類是用來操作URI的,核心方法有兩個:

// withAppendedId()作用:向URI追加一個id
Uri uri = Uri.parse("content://cn.scu.myprovider/user") 
Uri resultUri = ContentUris.withAppendedId(uri, 7);  
// 最終生成后的Uri為:content://cn.scu.myprovider/user/7

// parseId()作用:從URL中獲取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7") 
long personid = ContentUris.parseId(uri); 
//獲取的結果為:7

UriMatcher類

在ContentProvider里面注冊URI,根據URI匹配ContentProvider中對應的資料,具體使用:

// 步驟1:初始化UriMatcher物件
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 
    //常量UriMatcher.NO_MATCH  = 不匹配任何路徑的回傳碼
    // 即初始化時不匹配任何東西

// 步驟2:在ContentProvider 中注冊URI(addURI())
    int URI_CODE_a = 1int URI_CODE_b = 2;
    matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a); 
    matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b); 
    // 若URI資源路徑 = content://cn.scu.myprovider/user1 ,則回傳注冊碼URI_CODE_a
    // 若URI資源路徑 = content://cn.scu.myprovider/user2 ,則回傳注冊碼URI_CODE_b

// 步驟3:根據URI 匹配 URI_CODE,從而匹配ContentProvider中相應的資源(match())

@Override   
    public String getType(Uri uri) {   
      Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");   

      switch(matcher.match(uri)){   
     // 根據URI匹配的回傳碼是URI_CODE_a
     // 即matcher.match(uri) == URI_CODE_a
      case URI_CODE_a:   
        return tableNameUser1;   
        // 如果根據URI匹配的回傳碼是URI_CODE_a,則回傳ContentProvider中的名為tableNameUser1的表
      case URI_CODE_b:   
        return tableNameUser2;
        // 如果根據URI匹配的回傳碼是URI_CODE_b,則回傳ContentProvider中的名為tableNameUser2的表
    }   
}

ContentObserver類

觀察URI引起ContentProvider中的資料變化并且可以通知外界去更新資料,主要是由notifyChange(Uri uri)方法來實作通知

使用如下:

// 步驟1:注冊內容觀察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通過ContentResolver類進行注冊,并指定需要觀察的URI

// 步驟2:當該URI的ContentProvider資料發生變化時,通知外界(即訪問該ContentProvider資料的訪問者)
    public class UserContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
      db.insert("user", "userid", values); 
      getContext().getContentResolver().notifyChange(uri, null); 
      // 通知訪問者
   } 
}

// 步驟3:解除觀察者
 getContentResolver().unregisterContentObserver(uri);
    // 同樣需要通過ContentResolver類進行解除

實體

ContentProvider主要作用是行程通信,所以實體有兩個分別是行程內和行程間,

兩者相同的是創建資料庫類、自定義ContentProvider類

DBHelper.java用于創建資料庫

public class DBHelper extends SQLiteOpenHelper {

    // 資料庫名
    private static final String DATABASE_NAME = "finch.db";

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";

    private static final int DATABASE_VERSION = 1;
    //資料庫版本號

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        // 創建兩個表格:用戶表 和職業表
        db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
        db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {

    }
}

自定義ContentProvider類

public class MyProvider extends ContentProvider {

    private Context mContext;
    DBHelper mDbHelper = null;
    SQLiteDatabase db = null;

    public static final String AUTOHORITY = "cn.scu.myprovider";
    // 設定ContentProvider的唯一標識

    public static final int User_Code = 1;
    public static final int Job_Code = 2;

    // UriMatcher類使用:在ContentProvider 中注冊URI
    private static final UriMatcher mMatcher;
    static{
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mMatcher.addURI(AUTOHORITY,"user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
        // 若URI資源路徑 = content://cn.scu.myprovider/user ,則回傳注冊碼User_Code
        // 若URI資源路徑 = content://cn.scu.myprovider/job ,則回傳注冊碼Job_Code
    }

    // 以下是ContentProvider的6個方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {

        mContext = getContext();
        // 在ContentProvider創建時對資料庫進行初始化
        // 運行在主執行緒,故不能做耗時操作,此處僅作展示
        mDbHelper = new DBHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        // 初始化兩個表的資料(先清空兩個表,再各加入一個記錄)
        db.execSQL("delete from user");
        db.execSQL("insert into user values(1,'Carson');");
        db.execSQL("insert into user values(2,'Kobe');");

        db.execSQL("delete from job");
        db.execSQL("insert into job values(1,'Android');");
        db.execSQL("insert into job values(2,'iOS');");

        return true;
    }

    /**
     * 添加資料
     */

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
        // 該方法在最下面
        String table = getTableName(uri);

        // 向該表添加資料
        db.insert(table, null, values);

        // 當該URI的ContentProvider資料發生變化時,通知外界(即訪問該ContentProvider資料的訪問者)
        mContext.getContentResolver().notifyChange(uri, null);

//        // 通過ContentUris類從URL中獲取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        return uri;
        }

    /**
     * 查詢資料
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
        // 該方法在最下面
        String table = getTableName(uri);

//        // 通過ContentUris類從URL中獲取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        // 查詢資料
        return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    /**
     * 更新資料
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // 由于不展示,此處不作展開
        return 0;
    }

    /**
     * 洗掉資料
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 由于不展示,此處不作展開
        return 0;
    }

    @Override
    public String getType(Uri uri) {

        // 由于不展示,此處不作展開
        return null;
    }

    /**
     * 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = DBHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = DBHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
        }
    }

行程內的通信注冊

AndroidManifest.xml

<provider android:name="MyProvider"
          android:authorities="cn.scu.myprovider"/>

行程內訪問ContentProvider的資料

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /**
         * 對user表進行操作
         */

        // 設定URI
        Uri uri_user = Uri.parse("content://cn.scu.myprovider/user");

        // 插入表中資料
        ContentValues values = new ContentValues();
        values.put("_id", 3);
        values.put("name", "Iverson");


        // 獲取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入資料
        resolver.insert(uri_user,values);

        // 通過ContentResolver 向ContentProvider中查詢資料
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 將表中資料全部輸出
        }
        cursor.close();
        // 關閉游標

        /**
         * 對job表進行操作
         */
        // 和上述類似,只是URI需要更改,從而匹配不同的URI CODE,從而找到不同的資料資源
        Uri uri_job = Uri.parse("content://cn.scu.myprovider/job");
        
        // 插入表中資料
        ContentValues values2 = new ContentValues();
        values2.put("_id", 3);
        values2.put("job", "NBA Player");

        // 獲取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入資料
        resolver2.insert(uri_job,values2);

        // 通過ContentResolver 向ContentProvider中查詢資料
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 將表中資料全部輸出
        }
        cursor2.close();
        // 關閉游標
}
}

行程間的通信注冊

行程一的AndroidManifest.xml

<provider 
         android:name="MyProvider"
         android:authorities="scut.carson_ho.myprovider"

         // 宣告外界行程可訪問該Provider的權限(讀 & 寫)
         android:permission="scut.carson_ho.PROVIDER"             
               
         // 權限可細分為讀 & 寫的權限
         // 外界需要宣告同樣的讀 & 寫的權限才可進行相應操作,否則會報錯
         // android:readPermisson = "scut.carson_ho.Read"
         // android:writePermisson = "scut.carson_ho.Write"

         // 設定此provider是否可以被其他行程使用
         android:exported="true"/>

// 宣告本應用 可允許通信的權限
    <permission android:name="scut.carson_ho.Read" android:protectionLevel="normal"/>
    // 細分讀 & 寫權限如下,但本Demo直接采用全權限
    // <permission android:name="scut.carson_ho.Write" android:protectionLevel="normal"/>
    // <permission android:name="scut.carson_ho.PROVIDER" android:protectionLevel="normal"/>

行程二的AndroidManifest.xml需要添加相關的權限

    // 宣告本應用可允許通信的權限(全權限)
    <uses-permission android:name="scut.carson_ho.PROVIDER"/>

    // 細分讀 & 寫權限如下,但本Demo直接采用全權限
    // <uses-permission android:name="scut.carson_ho.Read"/>
    //  <uses-permission android:name="scut.carson_ho.Write"/>
    
// 注:宣告的權限必須與行程1中設定的權限對應

行程二的ContentProvider

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /**
         * 對user表進行操作
         */

        // 設定URI
        Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");

        // 插入表中資料
        ContentValues values = new ContentValues();
        values.put("_id", 4);
        values.put("name", "Jordan");


        // 獲取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入資料
        resolver.insert(uri_user,values);

        // 通過ContentResolver 向ContentProvider中查詢資料
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 將表中資料全部輸出
        }
        cursor.close();
        // 關閉游標

        /**
         * 對job表進行操作
         */
        // 和上述類似,只是URI需要更改,從而匹配不同的URI CODE,從而找到不同的資料資源
        Uri uri_job = Uri.parse("content://scut.carson_ho.myprovider/job");

        // 插入表中資料
        ContentValues values2 = new ContentValues();
        values2.put("_id", 4);
        values2.put("job", "NBA Player");

        // 獲取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入資料
        resolver2.insert(uri_job,values2);

        // 通過ContentResolver 向ContentProvider中查詢資料
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 將表中資料全部輸出
        }
        cursor2.close();
        // 關閉游標
    }
}

優點

  • ContentProvider為應用間的資料互動提供了一個安全的環境:允許把自己的應用資料根據需求開放給 其他應用 進行 增、刪、改、查,而不用擔心因為直接開放資料庫權限而帶來的安全問題

  • 采用ContentProvider方式,其 解耦了 底層資料的存盤方式,使得無論底層資料存盤采用何種方式,外界對資料的訪問方式都是統一的,這使得訪問簡單 & 高效( 如一開始資料存盤方式 采用 SQLite 資料庫,后來把資料庫換成 MongoDB,也不會對上層資料ContentProvider使用代碼產生影響 )

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JaFgUtQ2-1612606912813)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210204162349042.png)]

作業程序

Activity的作業程序

(1) Activity的所有 startActivity 多載方法最終都會呼叫 startActivityForResult,
(2) 呼叫 mInstrumentation.execStartActivity.execStartActivity()方法,
(3) 代碼中啟動Activity的真正實作是由ActivityManagerNative.getDefault().startActivity()方法完成的. ActivityManagerService簡稱AMS. AMS繼承自ActivityManagerNative(), 而ActivityManagerNative()繼承自Binder并實作了IActivityManager這個Binder介面, 因此AMS也是一個Binder, 它是IActivityManager的具體實作.ActivityManagerNative.getDefault()本質是一個IActivityManager型別的Binder物件, 因此具體實作是AMS.
(4) 在ActivityManagerNative中, AMS這個Binder物件采用單例模式對外提供, Singleton是一個單例封裝類. 第一次呼叫它的get()方法時會通過create方法來初始化AMS這個Binder物件, 在后續呼叫中會回傳這個物件.
(5) AMS的startActivity()程序

  • checkStartActivityResult () 方法檢查啟動Activity的結果( 包括檢查有無在
    manifest注冊)

  • Activity啟動程序經過兩次轉移, 最后又轉移到了mStackSupervisor.startActivityMayWait()這個方法, 所屬類為ActivityStackSupervisor. 在startActivityMayWait()內部又呼叫了startActivityLocked()這里會回傳結果碼就是之前checkStartActivityResult()用到的,

  • 方法最后會呼叫startActivityUncheckedLocked(), 然后又呼叫了ActivityStack#resumeTopActivityLocked(). 這個時候啟動程序已經從ActivityStackSupervisor轉移到了ActivityStack類中.

    img

    (6) 在最后的 ActivityStackSupervisor. realStartActivityLocked() 中,呼叫了 app.thread.scheduleLaunchActivity() 方法, 這個app.thread是ApplicationThread 型別,繼承于 IApplicationThread 是一個Binder類,內部是各種啟動/停止 Service/Activity的介面,

    (7) 在ApplicationThread中, scheduleLaunchActivity() 用來啟動Activity,里面的實作就是發送一個Activity的訊息( 封裝成 從ActivityClientRecord 物件) 交給Handler處理,這個Handler有一個簡潔的名字 H ,

    (8) 在H的 handleMessage() 方法里,通過 handleLaunchActivity() 方法完成Activity物件的創建和啟動,并且ActivityThread通過handleResumeActivity()方法來呼叫被啟動的onResume()這一生命周期方法,PerformLaunchActivity()主要完成了如下幾件事:

  • 從ActivityClientRecord物件中獲取待啟動的Activity組件資訊

  • 通過 Instrumentation 的 newActivity 方法使用類加載器創建Activity物件

  • 通過 LoadedApk 的makeApplication方法嘗試創建Application物件,通過類加載器實作( 如果Application已經創建過了就不會再創建)

  • 創建 ContextImpl 物件并通過Activity的 attach 方法完成一些重要資料的初始化(ContextImpl是一個很重要的資料結構, 它是Context的具體實作, Context中的大部分邏輯都是由ContentImpl來完成的. ContextImpl是通過Activity的attach()方法來和Activity建立關聯的,除此之外, 在attach()中Activity還會完成Window的創建并建立自己和Window的關聯, 這樣當Window接收到外部輸入事件收就可以將事件傳遞給Activity.)

  • 通過 mInstrumentation.callActivityOnCreate(activity, r.state) 方法呼叫Activity的 onCreate 方法

Service的作業程序

  • 啟動狀態:執行后臺計算
  • 系結狀態:用于其他組件與Service互動

兩種狀態是可以共存的

1. Service的啟動程序

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GrvZb2Br-1612606912820)(http://hujiaweibujidao.github.io/images/androidart_service1.png)]

image

(1) Service的啟動從 ContextWrapper 的 startService 開始
(2) 在ContextWrapper中,大部分操作通過一個 ContextImpl 物件mBase實作
(3) 在ContextImpl中, mBase.startService() 會呼叫 startServiceCommon 方法,而
startServiceCommon方法又會通過 ActivityManagerNative.getDefault() ( 實際上就是AMS) 這個物件來啟動一個服務,
(4) AMS會通過一個 ActiveService 物件( 輔助AMS進行Service管理的類,包括Service的啟動,系結和停止等) mServices來完成啟動Service: mServices.startServiceLocked() ,
(5) 在mServices.startServiceLocked()最后會呼叫 startServiceInnerLocked() 方法:將Service的資訊包裝成一個 ServiceRecord 物件,ServiceRecord一直貫穿著整個Service的啟動程序,通過 bringUpServiceLocked()方法來處理,bringUpServiceLocked()又呼叫了 realStartServiceLocked() 方法,這才真正地去啟動一個Service了,
(6) realStartServiceLocked()方法的作業如下:

  • app.thread.scheduleCreateService() 來創建Service并呼叫其onCreate()生命周期方法
  • sendServiceArgsLocked() 呼叫其他生命周期方法,如onStartCommand()
  • app.thread物件是 IApplicationThread 型別,實際上就是一個Binder,具體實作是ApplicationThread繼承ApplictionThreadNative

(7) 具體看Application對Service的啟動程序app.thread.scheduleCreateService():通過 sendMessage(H.CREATE_SERVICE , s) ,這個程序和Activity啟動程序類似,同時通過發送訊息給Handler H來完成的,
(8) H會接受這個CREATE_SERVICE訊息并通過ActivityThread的 handleCreateService() 來完成Service的最終啟動,
(9) handleCreateService()完成了以下作業:

  • 通過ClassLoader創建Service物件
  • 創建Service內部的Context物件
  • 創建Application,并呼叫其onCreate()( 只會有一次)
  • 通過 service.attach() 方法建立Service與context的聯系( 與Activity類似)
  • 呼叫service的 onCreate() 生命周期方法,至此,Service已經啟動了
  • 將Service物件存盤到ActivityThread的一個ArrayMap中

2. Service的系結程序

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-POL4RFHz-1612606912823)(http://hujiaweibujidao.github.io/images/androidart_service2.png)]

image

和service的啟動程序類似的:
(1) Service的系結是從 ContextWrapper 的 bindService 開始
(2) 在ContextWrapper中,交給 ContextImpl 物件 mBase.bindService()
(3) 最侄訓呼叫ContextImpl的 bindServiceCommon 方法,這個方法完成兩件事:

  • 將客戶端的ServiceConnection轉化成 ServiceDispatcher.InnerConnection 物件,ServiceDispatcher連接ServiceConnection和InnerConnection,這個程序通過 LoadedApk 的 getServiceDispatcher 方法來實作,將客戶端的ServiceConnection和ServiceDispatcher的映射關系存在一個ArrayMap中,
  • 通過AMS來完成Service的具體系結程序 ActivityManagerNative.getDefault().bindService()

(4) AMS中,bindService()方法再呼叫 bindServiceLocked() ,bindServiceLocked()再呼叫 bringUpServiceLocked() ,bringUpServiceLocked()又會呼叫 realStartServiceLocked() ,
(5) AMS的realStartServiceLocked()會呼叫 ActiveServices 的requrestServiceBindingLocked() 方法,最終是呼叫了ServiceRecord物件r的 app.thread.scheduleBindService() 方法,
(6) ApplicationThread的一系列以schedule開頭的方法,內部都通過Handler H來中轉:scheduleBindService()內部也是通過 sendMessage(H.BIND_SERVICE , s)
(7) 在H內部接收到BIND_SERVICE這類訊息時就交給 ActivityThread 的handleBindService() 方法處理:

  • 根據Servcie的token取出Service物件
  • 呼叫Service的 onBind() 方法,至此,Service就處于系結狀態了,
  • 這時客戶端還不知道已經成功連接Service,需要呼叫客戶端的binder物件來呼叫客戶端的ServiceConnection中的 onServiceConnected() 方法,這個通過 ActivityManagerNative.getDefault().publishService() 進行,ActivityManagerNative.getDefault()就是AMS,

(8) AMS的publishService()交給ActivityService物件 mServices 的 publishServiceLocked() 來處理,核心代碼就一句話 c.conn.connected(r.name,service) ,物件c的型別是 ConnectionRecord ,c.conn就是ServiceDispatcher.InnerConnection物件,service就是Service的onBind方法回傳的Binder物件,
(9) c.conn.connected(r.name,service)內部實作是交給了mActivityThread.post(new RunnConnection(name ,service,0)); 實作,ServiceDispatcher的mActivityThread是一個Handler,其實就是ActivityThread中的H,這樣一來RunConnection就經由H的post方法從而運行在主執行緒中,因此客戶端ServiceConnection中的方法是在主執行緒中被回呼的,
(10) RunConnection的定義如下:

  • 繼承Runnable介面, run() 方法的實作也是簡單呼叫了ServiceDispatcher的 doConnected 方法,
  • 由于ServiceDispatcher內部保存了客戶端的ServiceConntion物件,可以很方便地呼叫ServiceConntion物件的 onServiceConnected 方法,
  • 客戶端的onServiceConnected方法執行后,Service的系結程序也就完成了,
  • 根據步驟8、9、10service系結后通過ServiceDispatcher通知客戶端的程序可以說明ServiceDispatcher起著連接ServiceConnection和InnerConnection的作用, 至于Service的停止和解除系結的程序,系統流程都是類似的,

BroadcastReceiver的作業程序

簡單回顧一下廣播的使用方法, 首先定義廣播接收者, 只需要繼承BroadcastReceiver并重寫onReceive()方法即可. 定義好了廣播接收者, 還需要注冊廣播接收者, 分為兩種靜態注冊或者動態注冊. 注冊完成之后就可以發送廣播了.

1. 廣播的注冊程序

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-56GTjrdX-1612606912824)(http://hujiaweibujidao.github.io/images/androidart_broadcastreceiver1.png)]

image

(1) 動態注冊的程序是從ContextWrapper#registerReceiver()開始的. 和Activity或者Service一樣. ContextWrapper并沒有做實際的作業, 而是將注冊的程序直接交給了ContextImpl來完成.
(2) ContextImpl#registerReceiver()方法呼叫了本類的registerReceiverInternal()方法.
(3) 系統首先從mPackageInfo獲取到IIntentReceiver物件, 然后再采用跨行程的方式向AMS發送廣播注冊的請求. 之所以采用IIntentReceiver而不是直接采用BroadcastReceiver, 這是因為上述注冊程序中是一個行程間通信的程序. 而BroadcastReceiver作為Android中的一個組件是不能直接跨行程傳遞的. 所有需要通過IIntentReceiver來中轉一下.
(4) IIntentReceiver作為一個Binder介面, 它的具體實作是LoadedApk.ReceiverDispatcher.InnerReceiver, ReceiverDispatcher的內部同時保存了BroadcastReceiver和InnerReceiver, 這樣當接收到廣播的時候, ReceiverDispatcher可以很方便的呼叫BroadcastReceiver#onReceive()方法. 這里和Service很像有同樣的類, 并且內部類中同樣也是一個Binder介面.
(5) 由于注冊廣播真正實作程序是在AMS中, 因此跟進AMS中, 首先看registerReceiver()方法, 這里只關心里面的核心部分. 這段代碼最侄訓把遠程的InnerReceiver物件以及IntentFilter物件存盤起來, 這樣整個廣播的注冊就完成了.

2. 廣播的發送和接收程序

廣播的發送有幾種:普通廣播、有序廣播和粘性廣播,他們的發送/接收流程是類似的,因此只分析普通廣播的實作,

(1) 廣播的發送和接收, 本質就是一個程序的兩個階段. 廣播的發送仍然開始于ContextImpl#sendBroadcase()方法, 之所以不是Context, 那是因為Context#sendBroad()是一個抽象方法. 和廣播的注冊程序一樣, ContextWrapper#sendBroadcast()仍然什么都不做, 只是把事情交給了ContextImpl去處理.
(2) ContextImpl里面也幾乎什么都沒有做, 內部直接向AMS發起了一個異步請求用于發送廣播.
(3) 呼叫AMS#broadcastIntent()方法,繼續呼叫broadcastIntentLocked()方法,
(4) 在broadcastIntentLocked()內部, 會根據intent-filter查找出匹配的廣播接收者并經過一系列的條件過濾. 最侄訓將滿足條件的廣播接收者添加到BroadcastQueue中, 接著BroadcastQueue就會將廣播發送給相應廣播接收者.
(5) BroadcastQueue#scheduleBroadcastsLocked()方法內并沒有立即發送廣播, 而是發送了一個BROADCAST_INTENT_MSG型別的訊息, BroadcastQueue收到訊息后會呼叫processNextBroadcast()方法,
(6) 無序廣播存盤在mParallelBroadcasts中, 系統會遍歷這個集合并將其中的廣播發送給他們所有的接收者, 具體的發送程序是通過deliverToRegisteredReceiverLocked()方法實作. deliverToRegisteredReceiverLocked()負責將一個廣播發送給一個特定的接收者, 它的內部呼叫了performReceiverLocked方法來完成具體發送程序.
(7) performReceiverLocked()方法呼叫的ApplicationThread#scheduleRegisteredReceiver()實作比較簡單, 它通過InnerReceiver來實作廣播的接收
(8) scheduleRegisteredReceiver()方法中,receiver.performReceive()中的receiver對應著IIntentReceiver型別的介面. 而具體的實作就是ReceiverDispatcher I n n e r R e c e i v e r . 這 兩 個 嵌 套 的 內 部 類 是 所 屬 在 L o a d e d A p k 中 的 , ( 9 ) 又 調 用 了 L o a d e d A p k InnerReceiver. 這兩個嵌套的內部類是所屬在LoadedApk中的, (9) 又呼叫了LoadedApk InnerReceiver.LoadedApk(9)調LoadedApkReceiverDispatcher#performReceive()的方法.在performReceiver()這個方法中, 會創建一個Args物件并通過mActivityThread的post方法執行args中的邏輯. 而這些類的本質關系就是:

  • Args: 實作類Runnable
  • mActivityThread: 是一個Handler, 就是ActivityThread中的mH. mH就是ActivityThread$H. 這個內部類H以前說過.

(10) 實作Runnable介面的Args中BroadcastReceiver#onReceive()方法被執行了, 也就是說應用已經接收到了廣播, 同時onReceive()方法是在廣播接收者的主執行緒中被呼叫的.

android 3.1開始就增添了兩個標記為. 分別是FLAG_INCLUDE_STOPPED_PACKAGES, FLAG_EXCLUDE_STOPPED_PACKAGES. 用來控制廣播是否要對處于停止的應用起作用.

  • FLAG_INCLUDE_STOPPED_PACKAGES: 包含停止應用, 廣播會發送給已停止的應用.
  • FLAG_EXCLUDE_STOPPED_PACKAGES: 不包含已停止應用, 廣播不會發送給已停止的應用

在android 3.1開始, 系統就為所有廣播默認添加了FLAG_EXCLUDE_STOPPED_PACKAGES標識, 當這兩個標記共存的時候以FLAG_INCLUDE_STOPPED_PACKAGES(非默認項為主).

應用處于停止分為兩種

  • 應用安裝后未運行
  • 被手動或者其他應用強停
    開機廣播同樣受到了這個標志位的影響. 從Android 3.1開始處于停止狀態的應用同樣無法接受到開機廣播, 而在android 3.1之前處于停止的狀態也是可以接收到開機廣播的.

更多參考1、參考2、參考3

ContentProvider的作業機制

ContentProvider是一種內容共享型組件, 它通過Binder向其他組件乃至其他應用提供資料. 當ContentProvider所在的行程啟動時, ContentProvider會同時啟動并發布到AMS中. 要注意:這個時候ContentProvider的onCreate()方法是先于Application的onCreate()執行的,這一點在四大組件是少有的現象.

image

image

(1) 當一個應用啟動時,入口方法是ActivityThread的main方法,其中創建ActivityThread的實體并創建主執行緒的訊息佇列;
(2) ActivityThread的attach方法中會遠程呼叫ActivityManagerService的attachApplication,并將ApplicationThread提供給AMS,ApplicationThread主要用于ActivityThread和AMS之間的通信;
(3) ActivityManagerService的attachApplication會呼叫ApplicationThread的bindApplication方法,這個方法會通過H切換到ActivityThread中去執行,即呼叫handleBindApplication方法;
(4) handleBindApplication方法會創建Application物件并加載ContentProvider,注意是先加載ContentProvider,然后呼叫Application的onCreate方法,
(5) ContentProvider啟動后, 外界就可以通過它所提供的增刪改查這四個介面來操作ContentProvider中的資料源, 這四個方法都是通過Binder來呼叫的, 外界無法直接訪問ContentProvider, 它只能通過AMS根據URI來獲取到對應的ContentProvider的Binder介面IContentProvider, 然后再通過IContentProvider來訪問ContentProvider中的資料源.

ContentProvider的android:multiprocess屬性決定它是否是單實體,默認值是false,也就是默認是單實體,當設定為true時,每個呼叫者的行程中都存在一個ContentProvider物件,

當呼叫ContentProvider的insert、delete、update、query方法中的任何一個時,如果ContentProvider所在的行程沒有啟動的話,那么就會觸發ContentProvider的創建,并伴隨著ContentProvider所在行程的啟動,

以query呼叫為例

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QCLz1Wpd-1612606912828)(http://hujiaweibujidao.github.io/images/androidart_contentprovider.png)]

(1) 首先會獲取IContentProvider物件, 不管是通過acquireUnstableProvider()方法還是直接通過acquireProvider()方法, 他們的本質都是一樣的, 最終都是通過acquireProvider方法來獲取ContentProvider.
(2) ApplicationContentResolver#acquireProvider()方法并沒有處理任何邏輯, 它直接呼叫了ActivityThread#acquireProvider()
(3) 從ActivityThread中查找是否已經存在了ContentProvider了, 如果存在那么就直接回傳. ActivityThread中通過mProviderMap來存盤已經啟動的ContentProvider物件, 這個集合的存盤型別ArrayMap mProviderMap. 如果目前ContentProvider沒有啟動, 那么就發送一個行程間請求給AMS讓其啟動專案目標ContentProvider, 最后再通過installProvider()方法來修改參考計數.
(4) AMS是如何啟動ContentProvider的呢?首先會啟動ContentProvider所在的行程, 然后再啟動ContentProvider. 啟動行程是由AMS#startProcessLocked()方法來完成, 其內部主要是通過Process#start()方法來完成一個新行程的啟動, 新行程啟動后其入口方法為ActivityThread#main()方法,
(5) ActivityThread#main()是一個靜態方法, 在它的內部首先會創建ActivityThread實體并呼叫attach()方法來進行一系列初始化, 接著就開始進行訊息回圈. ActivityThread#attach()方法會將Application物件通過AMS#attachApplication方法跨行程傳遞給AMS, 最終AMS會完成ContentProvider的創建程序.
(6) AMS#attachApplication()方法呼叫了attachApplication(), 然后又呼叫了ApplicationThread#bindApplication(), 這個程序也屬于行程通信.bindApplication()方法會發送一個BIND_APPLICATION型別的訊息給mH, 這是一個Handler, 它收到訊息后會呼叫ActivityThread#handleBindApplication()方法.
(7) ActivityThread#handlerBindApplication()則完成了Application的創建以及ContentProvider 可以分為如下四個步驟:

  • 創建ContentProvider和Instrumentation
  • 創建Application物件
  • 啟動當前行程的ContentProvider并呼叫onCreate()方法. 主要內部實作是installContentProvider()完成了ContentProvider的啟動作業, 首先會遍歷當前行程的ProviderInfo的串列并一一呼叫installProvider()方法來啟動他們, 接著將已經啟動的ContentProvider發布到AMS中, AMS會把他們存盤在ProviderMap中, 這樣一來外部呼叫者就可以直接從AMS中獲取到ContentProvider. installProvider()內部通過類加載器創建的ContentProvider實體并在方法中呼叫了attachInfo(), 在這內部呼叫了ContentProvider#onCreate()
  • 呼叫Application#onCreate()

經過了上述的四個步驟, ContentProvider已經啟動成功, 并且其所在的行程的Application也已經成功, 這意味著ContentProvider所在的行程已經完成了整個的啟動程序, 然后其他應用就可以通過AMS來訪問這個ContentProvider了.

當拿到了ContentProvider以后, 就可以通過它所提供的介面方法來訪問它. 這里要注意: 這里的ContentProvider并不是原始的ContentProvider. 而是ContentProvider的Binder型別物件IContentProvider, 而IContentProvider的具體實作是ContentProviderNative和ContentProvider.Transport. 后者繼承了前者.

如果還用query方法來解釋流程: 那么最開始其他應用通過AMS獲取到ContentProvider的Binder物件就是IContentProvider. 而IContentProvider的實際實作者是ContentProvider.Transport. 因此實際上外部應用呼叫的時候本質上會以行程間通信的方式呼叫ContentProvider.Transport的query()方法,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/257822.html

標籤:其他

上一篇:均分糖果——2021年2-7更

下一篇:【EOJ Monthly】2021.2

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more