Android APK安裝流程(1)

前言

  • android系統里app有哪些類型及其安裝涉及目錄、所需權限是什么?
  • apk安裝有幾種方式?
  • apk安裝流程會涉及到哪些android系統知識?
  • apk安裝的過程大體上分哪幾步?
  • apk安裝的過程中涉及到比較重要的類有哪些?分別用來做什么?
  • 了解apk安裝流程有什么用?

Android系統里app類型、安裝涉及目錄、所需權限

1./system/framwork:
  • 保存的是資源型的應用程序,它們用來打包資源文件
2./system/app:
  • 系統自帶的應用程序,獲得adb root 權限才能刪除。
  • 如果想修改該目錄下的app,必須手動push新的apk進去,該新app文件不會自動被安裝,需要系統重啟時,系統檢查到apk被更新,才會去安裝。
  • 系統app升級時,實際上是在/data/app里重新安裝了一個app,只是這個路徑會重新注冊到系統那里去,當打開該系統app時,會指向新app的地址。
  • 如果你卸載該更新后的系統app,系統只是卸載/data/app里的app,然后恢復/system/app里的app給用戶使用。
3./system/priv-app:
  • 這里放的是權限更高的系統核心應用,例如系統設置、系統UI、開機launcher等,這個目錄非常重要,一般情況下不建議改動。
4./vendor/app:
  • 保存設備廠商提供的應用程序
5./data/app:
  • 用戶程序安裝的目錄,安裝時把apk文件復制到此目錄。因為Android機有內部存儲和SD卡兩部分,很多Android機為了節省內存空間,會把apk安裝到SD卡上。如此以來,/data/app在大部分Android機上會有2個,1個在內部存儲1個在SD卡里。
6./data/app-private:
  • 保存受DRM保護的私有應用程序,例如一個受保護的歌曲或受保護的視頻是使用 DRM保護的文件。
7./data/data:
  • 存放應用程序數據的目錄
8./data/dalvik-cache:
  • 存放apk中的dex文件。dex文件是dalvik虛擬機的可執行文件,其大小約為原始apk文件大小的四分之一,但是ART-Android Runtime的可執行文件格式為.oat,所以啟動ART時,系統會執行dex文件轉換至oat文件
9./data/system:

該目錄主要是存放一些系統文件

  • packages.xml文件:類似于Window的注冊表,這個文件是解析apk時由writeLP()創建的,里面記錄了系統的permissons,以及每個apk的name,codePath,flag,ts,version,userid,native 庫的存儲路徑等信息,這些信息主要通過apk的AndroidManifest解析獲取,解析完apk后將更新信息寫入這個文件并保存到flash,下次開機的時候直接從里面讀取相關信息并添加到內存相關列表中。當有apk升級,安裝或刪除時會更新這個文件。
  • pakcages-back.xml:packages.xml文件的備份。
  • pakcages-stoped.xml:記錄系統中被強制停止的運行的應用信息,系統在強制停止某個應用的時候,會將應用的信息記錄在該文件中。
  • pakcages-stoped-backup.xml:pakcages-stoped.xml文件的備份。
  • package.list:packages.list指定了應用的默認存儲位置/data/data/com.xxx.xxx;
  • 這5個文件中pakcages-back.xml和pakcages-stoped-backup.xml是備份文件。當Android對文件packages.xml和pakcages-stoped.xml寫之前,會先把它們備份,如果寫文件成功了,再把備份文件刪除。如果寫的時候,系統出問題了,重啟后在需要讀取這兩個文件時,如果發現備份文件存在,會使用備份文件的內容,因為源文件可能已經損壞了。其中packages.xml是PackageManagerServcie啟動時,需要用到的文件。

APK安裝主體流程

apk_install_structure.png

1.復制APK到/data/app目錄下,解壓并掃描安裝包
2.將APP的dex文件拷貝到/data/dalvik-cache目錄,再在/data/data/目錄下創建應用程序的數據目錄(以應用包名命令),用來存放應用的數據庫、xml文件、cache、二進制的so動態庫等
3.解析apk的AndroidManifest.xml文件,注冊四大組件,將apk的權限、應用包名、apk的安裝位置、版本、userID等重要信息保存在/data/system/packages.xml文件中。這些操作都是在PackageManagerService中完成。
4.資源管理器解析APK里的資源文件。
5.dex2oat操作,對dex文件進行優化,并保存在dalvik-cache目錄下。
6.更新權限信息。
7.安裝完成后,發送Intent.ACTION_PACKAGE_ADDED廣播。
8.顯示icon圖標,應用程序經過PMS中的邏輯處理后,相當于已經注冊好了,如果想要在Android桌面上看到icon圖標,則需要Launcher將系統中已經安裝的程序展現在桌面上。

總體說來就兩件事情拷貝APK和解析APK,解析APK主要是解析APK的應用配置文件AndroidManifest.xml,以便獲得它的安裝信息。在安裝的過程中還會這個應用分配Linux用戶ID和Linux用戶組ID(以便它可以在系統中獲取合適的運行權限)。

Tips,dexopt/dex2oat 操作在不同版本下的區別:
  • Dalvik虛擬機-dexopt: 該時機在第一次執行時app時 非安裝時(詳情見 Android運行流程)
  • ART虛擬機-dex2oat:AOT (Ahead-Of-Time) 運行前編譯
    • Android O(8.0)前,將dex字節碼翻譯成本地字節碼oat(非全量)
    • Android O(8.0)開始:開始新增vdex概念,不是為了提升性能,而是為了避免不必要的驗證Dex 文件合法性的過程,例如首次安裝時進行dex2oat時會校驗Dex 文件各個section的合法性,這時候使用的compiler filter 為了照顧安裝速度等方面,并沒有采用全量編譯,當app盤啟動后,運行一段時間后,收集了足夠多的jit 熱點方法信息,Android會在后臺重新進行dex2oat,將熱點方法編譯成機器代碼,這時候就不用再重復做驗證Dex文件的過程了。

APK安裝的幾種方式

  • 1、系統安裝

系統啟動后調用 PackageManagerService.main() 初始化注冊解析安裝工作。PackageManagerService處理各種應用的安裝,卸載,管理等工作,開機時由systemServer啟動此服務。
第1步:PackageManagerService.main()初始化注冊
第2步:建立Java層的installer與C層的intalld的socket聯接
第3步:建立PackageHandler消息循環
第4步:調用成員變量mSettings的readLPw()方法恢復上一次的安裝信息
第5步:.jar文件的detopt優化
第6步:scanDirLI函數掃描特定目錄的APK文件解析
第7步:updatePermissionsLPw()函數分配權限
第8步:調用mSettings.writeLPr()保存安裝信息

  • 2、adb 命令安裝

通過 pm 參數,調用 PM 的 runInstall 方法,進入 PackageManagerService 進行安裝工作。
第1步:pm.java的runInstall()方法
第2步:參數不對會調用showUsage方法,彈出使用說明
第3步:正常情況runInstall會調用mPm變量的installPackageWithVerification方法
第4步:由于pm.java中的變量mPm是PackageManagerService的實例,所以實際上是調用PackageManagerService的installPackageWithVerfication()方法
第5步:進入PackageManagerService的doHandleMessage方法的INIT_COPY分支
第6步:成功綁定了com.android.defcontainer.DefaultContainerService服務,進入MCS_BOUND分支
第7步:里面調用PackageManagerService中內部抽象類HandlerParams的子類InstallParams的startCopy方法。
第8步:抽象類的HandlerParams的startCopy方法調用了HandlerParams子類的handleStartCopy和handlerReturnCode兩個方法
第9步:handlesStartCopy方法調用了InstallArgs的子類copyApk,它負責將下載的APK文件copy到/data/app
第10步:handleReturnCode調用handleReturnCode方法
第11步:調用PackageManagerService服務的installPackageLI(PackageParser.Package, int, int, UserHandle, String, String,PackageInstalledInfo)方法進行APK掃描。
第12步:上面的方法判斷是否APP應安裝,調用installNewPackageLI或replacePackageLI方法
第13步:調用updateSettingsLI方法進行更新PackageManagerService的Settings
第14步:發送what值為POST_INSTALL的Message給PackageHandler進行處理
第15步:發送what值為MCS_UNBIND的Message給PackageHandler,進而調用PackageHandler.disconnectService()中斷連接

  • 3、應用市場安裝

這個要視應用的權限,有系統的權限無安裝界面(例如MiUI的小米應用商店)。
第1步:調用PackageManagerService的installPackage方法
第2步:上面的方法調用installPackageWithVerfication(),進行權限校驗,發送INIT_COPY的msg
第3步:進入PackageManagerService的doHandleMessage方法的INIT_COPY分支
第4步:成功綁定了com.android.defcontainer.DefaultContainerService服務,進入MCS_BOUND分支
第5步:里面調用PackageManagerService中內部抽象類HandlerParams的子類InstallParams的startCopy方法。
第6步:抽象類的HandlerParams的startCopy方法調用了HandlerParams子類的handleStartCopy和handlerReturnCode兩個方法
第7步:handlesStartCopy方法調用了InstallArgs的子類copyApk,它負責將下載的APK文件copy到/data/app
第8步:handleReturnCode調用handleReturnCode方法
第9步:調用PackageManagerService服務的installPackageLI(PackageParser.Package, int, int, UserHandle, String, String,PackageInstalledInfo)方法進行APK掃描。
第10步:上面的方法判斷是否APP應安裝,調用installNewPackageLI或replacePackageLI方法
第11步:調用updateSettingsLI方法進行更新PackageManagerService的Settings
第12步:發送what值為POST_INSTALL的Message給PackageHandler進行處理
第13步:發送what值為MCS_UNBIND的Message給PackageHandler,進而調用PackageHandler.disconnectService()中斷連接

  • 4、第三方安裝

有安裝界面,通過PackageInstaller.apk來處理,安裝及卸載的過程的界面先調用 InstallStart(是一個Activity) 進行權限檢查之后啟動 PackageInstallActivity,調用 PackageInstallActivity 的 startInstall 方法,點擊 OK 按鈕后進入 PackageManagerService 完成拷貝解析安裝工作。
第1步:通過隱式跳轉啟動InstallStart
第2步:在InstallStart的onCreate生命周期里判斷如果不允許未知來源的則阻止安裝、targetSdkVersion<0阻止安裝,如果targetSdkVersion大于等于26(8.0), 且獲取不到REQUEST_INSTALL_PACKAGES權限也中止安裝,然后啟動PackageInstallerActivity
第3步:調用PackageInstallerActivity的onCreate方法初始化安裝界面
第4步:初始化界面以后調用initiateInstall方法
第5步:上面的方法調用startInstallConfirm方法,彈出確認和取消安裝的按鈕
第6步:點擊確認按鈕,打開新的activity:InstallAppProgress
第7步:InstallAppProgress類初始化帶有進度條的界面之后,調用PackageManager的installPackage方法
第8步:PackageManager是PackageManagerService實例,所以就是調用PackageManagerService的installPackage方法
第9步:調用PackageManagerService的installPackage方法
第10步:上面的方法調用installPackageWithVerfication(),進行權限校驗,發送INIT_COPY的msg
第11步:進入PackageManagerService的doHandleMessage方法的INIT_COPY分支
第12步:成功綁定了com.android.defcontainer.DefaultContainerService服務,進入MCS_BOUND分支
第13步:里面調用PackageManagerService中內部抽象類HandlerParams的子類InstallParams的startCopy方法。
第14步:抽象類的HandlerParams的startCopy方法調用了HandlerParams子類的handleStartCopy和handlerReturnCode兩個方法
第15步:handlesStartCopy方法調用了InstallArgs的子類copyApk,它負責將下載的APK文件copy到/data/app
第16步:handleReturnCode調用handleReturnCode方法
第17步:調用PackageManagerService服務的installPackageLI(PackageParser.Package, int, int, UserHandle, String, String,PackageInstalledInfo)方法進行APK掃描。
第18步:上面的方法判斷是否APP應安裝,調用installNewPackageLI或replacePackageLI方法
第19步:調用updateSettingsLI方法進行更新PackageManagerService的Settings
第20步:發送what值為POST_INSTALL的Message給PackageHandler進行處理
第21步:發送what值為MCS_UNBIND的Message給PackageHandler,進而調用PackageHandler.disconnectService()中斷連接

以上4種安裝apk方式最終都會走到PackageManagerService,其中第4種方式走的流程最長最完整,所以下面我們會用第4種方式的流程進行代碼詳細分析。

APK安裝詳細代碼流程(android-10.0.0_r14)

  • 第1步 通過隱式跳轉啟動InstallStart

InstallStart是從Android8.0開始PackageInstaller.apk的入口Activity,7.0 隱式匹配的 Activity 是 PackageInstallerActivity。
platform/frameworks/base/packages/PackageInstaller/AndroidManifest.xml

        ...
        <activity android:name=".InstallStart"
                android:theme="@android:style/Theme.Translucent.NoTitleBar"
                android:exported="true"
                android:excludeFromRecents="true">
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.INSTALL_PACKAGE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="content" />
                <data android:mimeType="application/vnd.android.package-archive" />
            </intent-filter>
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.INSTALL_PACKAGE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="package" />
                <data android:scheme="content" />
            </intent-filter>
            <intent-filter android:priority="1">
                <action android:name="android.content.pm.action.CONFIRM_INSTALL" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        ...

從上面的Manifest文件我們可以看出除了通過android:mimeType=application/vnd.android.package-archive能啟動入口Activity-InstallStart,還能通過action為android.intent.action.INSTALL_PACKAGE的其他Scheme以及"android.content.pm.action.CONFIRM_INSTALL啟動,其中定義了兩個 scheme:content 和 package,這2種scheme數據會在后面流程里的PackageInstallerActivity里解析通過不同的方式獲取到安裝包信息,最后在InstallInstalling再通過不同的安裝方式來完成安裝動作(后續會有詳細分析)。

  • content: 在 file 協議的處理中調用了 PackageUtil.getPackageInfo 方法,方法內部調用了 PackageParser.parsePackage() 把 APK 文件的 manifest 和簽名信息都解析完成并保存在了 Package,Package 包含了該 APK 的所有信息,調用 PackageParser.generatePackageInfo 生成 PackageInfo。

  • package: 在 package 協議中調用了 PackageManager.getPackageInfo 方法生成 PackageInfo,PackageInfo 是跨進程傳遞的包數據(activities、receivers、services、providers、permissions等等)包含 APK 的所有信息。

  • 第2步 InstallStart啟動,進入其onCreate()生命周期

其實onCreate()生命周期里主要做了如下事情:
1.判斷當前的apk包是否是可信任來源,如果是不可信任來源,且沒有申請不可信任來源包安裝權限,則會強制結束當前的Activity。
2.此時就會從data從獲取到我們傳遞進來的apk包的地址uri,并且設置下一個啟動的Activity為InstallStaging。
3.啟動InstallStaging 這個Activity。
源碼如下:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mIPackageManager = AppGlobals.getPackageManager();
        Intent intent = getIntent();
        String callingPackage = getCallingPackage();

        final boolean isSessionInstall =
                PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());

        // If the activity was started via a PackageInstaller session, we retrieve the calling
        // package from that session
        final int sessionId = (isSessionInstall
                ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
                : -1);
        if (callingPackage == null && sessionId != -1) {
            PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
            PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
            callingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null;
        }

        final ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
        final int originatingUid = getOriginatingUid(sourceInfo);
        boolean isTrustedSource = false;
        // 判斷是否勾選“未知來源”選項
        if (sourceInfo != null
                && (sourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
            isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
        }

        if (!isTrustedSource && originatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
            final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid);
            // 如果targetSdkVerison小于0中止安裝
            if (targetSdkVersion < 0) {
                Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid);
                // Invalid originating uid supplied. Abort install.
                mAbortInstall = true;
            // 如果targetSdkVersion大于等于26(8.0), 且獲取不到REQUEST_INSTALL_PACKAGES權限中止安裝
            } else if (targetSdkVersion >= Build.VERSION_CODES.O && !declaresAppOpPermission(
                    originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
                Log.e(LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission "
                        + Manifest.permission.REQUEST_INSTALL_PACKAGES);
                mAbortInstall = true;
            }
        }
        if (mAbortInstall) {
            setResult(RESULT_CANCELED);
            finish();
            return;
        }

        Intent nextActivity = new Intent(intent);
        nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);

        // The the installation source as the nextActivity thinks this activity is the source, hence
        // set the originating UID and sourceInfo explicitly
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
        nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
        // 如果設置了ACTION_CONFIRM_INSTALL,則調用PackageInstallerActivity
        if (isSessionInstall) {
            nextActivity.setClass(this, PackageInstallerActivity.class);
        } else {
            Uri packageUri = intent.getData();
            // 判斷Uri的Scheme協議是否是content
            if (packageUri != null && packageUri.getScheme().equals(
                    ContentResolver.SCHEME_CONTENT)) {
                // 這個路徑已經被起用了,但是仍然可以工作
                // [IMPORTANT] This path is deprecated, but should still work. Only necessary
                // features should be added.

                // Copy file to prevent it from being changed underneath this process


                // 調用InstallStaging來拷貝file/content,防止被修改
                nextActivity.setClass(this, InstallStaging.class);
            } else if (packageUri != null && packageUri.getScheme().equals(
                    PackageInstallerActivity.SCHEME_PACKAGE)) {
                // 如果Uri中包含package,則調用PackageInstallerActivity
                nextActivity.setClass(this, PackageInstallerActivity.class);
            } else {
                // Uri不合法
                Intent result = new Intent();
                result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                        PackageManager.INSTALL_FAILED_INVALID_URI);
                setResult(RESULT_FIRST_USER, result);

                nextActivity = null;
            }
        }

        if (nextActivity != null) {
            startActivity(nextActivity);
        }
        finish();
    }

從以上源碼我們可以看出:
1.如果我們使用另一種安裝方式,設置Intent的action為PackageInstaller.ACTION_CONFIRM_INSTALL(也就是android.content.pm.action.CONFIRM_INSTALL),則會直接啟動PackageInstallerActivity
2.如果Scheme是package協議也是直接啟動PackageInstallerActivity
3.只有當Scheme是content時才會跳轉到InstallStaging(即使跳轉到InstallStaging,最后還是會跳轉到PackageInstallerActivity,所以第3步我們會分析跳轉到InstallStaging的代碼)

  • 第3步 啟動InstallStaging,進入其onResume()生命周期

看其onResume()方法(核心實現)

    @Override
    protected void onResume() {
        super.onResume();
        // This is the first onResume in a single life of the activity
        if (mStagingTask == null) {
            // File does not exist, or became invalid
            if (mStagedFile == null) {
                // Create file delayed to be able to show error
                try {
                    mStagedFile = TemporaryFileManager.getStagedFile(this);
                } catch (IOException e) {
                    showError();
                    return;
                }
            }
            mStagingTask = new StagingAsyncTask();
            mStagingTask.execute(getIntent().getData());
        }
    }

在這里通過TemporaryFileManager.getStagedFile(this)方法構建了一個臨時文件:

  /**
     * Create a new file to hold a staged file.
     *
     * @param context The context of the caller
     *
     * @return A new file
     */
    @NonNull
    public static File getStagedFile(@NonNull Context context) throws IOException {
        return File.createTempFile("package", ".apk", context.getNoBackupFilesDir());
    }

這個文件建立在當前應用私有目錄下的no_backup文件夾上。也就是/data/no_backup/packagexxx.apk 這個臨時文件。

這有一個StagingAsyncTask類:

    private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
        @Override
        protected Boolean doInBackground(Uri... params) {
            if (params == null || params.length <= 0) {
                return false;
            }
            Uri packageUri = params[0];
            try (InputStream in = getContentResolver().openInputStream(packageUri)) {
                // Despite the comments in ContentResolver#openInputStream the returned stream can
                // be null.
                if (in == null) {
                    return false;
                }

                try (OutputStream out = new FileOutputStream(mStagedFile)) {
                    byte[] buffer = new byte[1024 * 1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) {
                        // Be nice and respond to a cancellation
                        if (isCancelled()) {
                            return false;
                        }
                        out.write(buffer, 0, bytesRead);
                    }
                }
            } catch (IOException | SecurityException | IllegalStateException e) {
                Log.w(LOG_TAG, "Error staging apk from content URI", e);
                return false;
            }
            return true;
        }

        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                // Now start the installation again from a file
                Intent installIntent = new Intent(getIntent());
                installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
                installIntent.setData(Uri.fromFile(mStagedFile));

                if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
                    installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                }

                installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
                startActivity(installIntent);

                InstallStaging.this.finish();
            } else {
                showError();
            }
        }
    }

StagingAsyncTask主要工作:
1.doInBackground 是指在異步線程池中處理的事務。doInBackground中實際上就是把uri中需要安裝的apk拷貝到臨時文件中(上文的/data/no_backup/packagexxx.apk)。
2.onPostExecute 當拷貝任務處理完之后,就會把當前的臨時文件Uri作為Intent的參數(這個時候會把Uri的類型設置為file),跳轉到DeleteStagedFileOnResult中。

  • 第4步 啟動DeleteStagedFileOnResult

看其源碼:

/**
 * Trampoline activity. Calls PackageInstallerActivity and deletes staged install file onResult.
 */
public class DeleteStagedFileOnResult extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            Intent installIntent = new Intent(getIntent());
            installIntent.setClass(this, PackageInstallerActivity.class);

            installIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
            startActivityForResult(installIntent, 0);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        File sourceFile = new File(getIntent().getData().getPath());
        sourceFile.delete();

        setResult(resultCode, data);
        finish();
    }
}

這個Activity就非常簡單了,只是作為一個過渡,啟動真正大頭戲份PackageInstallerActivity。如果PackageInstallerActivity安裝失敗了,就會退出PackageInstallerActivity界面返回到DeleteStagedFileOnResult的onActivityResult中刪除這個臨時文件。

  • 第5步 啟動PackageInstallerActivity

這個類是真正的安裝界面,我們先來看其onCreate()方法:

    @Override
    protected void onCreate(Bundle icicle) {
        getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);

        super.onCreate(null);

        if (icicle != null) {
            mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY);
        }

        mPm = getPackageManager();
        mIpm = AppGlobals.getPackageManager();
        mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        mInstaller = mPm.getPackageInstaller();
        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);

        final Intent intent = getIntent();

        mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
        mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
        mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                PackageInstaller.SessionParams.UID_UNKNOWN);
        mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
                ? getPackageNameForUid(mOriginatingUid) : null;


        final Uri packageUri;

        if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction())) {
            final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
            final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
            if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
                Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
                finish();
                return;
            }

            mSessionId = sessionId;
            packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
            mOriginatingURI = null;
            mReferrerURI = null;
        } else {
            mSessionId = -1;
            packageUri = intent.getData();
            mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
        }

        // if there's nothing to do, quietly slip into the ether
        if (packageUri == null) {
            Log.w(TAG, "Unspecified source");
            setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
            finish();
            return;
        }

        if (DeviceUtils.isWear(this)) {
            showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
            return;
        }

        boolean wasSetUp = processPackageUri(packageUri);
        if (!wasSetUp) {
            return;
        }

        // load dummy layout with OK button disabled until we override this layout in
        // startInstallConfirm
        bindUi();
        checkIfAllowedAndInitiateInstall();
    }

從上面的代碼我們可以看出:
1.如果使用PackageInstaller.ACTION_CONFIRM_INSTALL(也就是第1步里InstallStart接收到的action一直傳下來的)模式進行安裝,那么就會獲取保存在Intent中的ApplicationInfo對象,獲取其中的resolvedBaseCodePath也就是代碼文件路徑。這種處理文件的方式會通過Uri.fromFile(new File(info.resolvedBaseCodePath))將Uri的Scheme設置為file類型(這個file類型會在下個Activity里用到)。
2.如果是使用android.intent.action.INSTALL_PACKAGE,也就是帶有Scheme的情況,則通過Intent的getData獲取到保存在其中的臨時文件(就是上一步中說的/data/no_backup/packagexxx.apk)。通過這種方式獲取到的PackageUri的Scheme是package類型。
3.上面2步主要是為了獲取代碼文件,形成PackageUri,然后通過方法processPackageUri() 掃描路徑對應的文件包,實例化PackageInfo對象mPkgInfo(安裝的必要屬性)。
4.bindUi()初始化安裝UI
5.checkIfAllowedAndInitiateInstall()啟動安裝

下面是processPackageUri()方法源碼:

    /**
     * Parse the Uri and set up the installer for this package.
     *
     * @param packageUri The URI to parse
     *
     * @return {@code true} iff the installer could be set up
     */
    private boolean processPackageUri(final Uri packageUri) {
        mPackageURI = packageUri;

        final String scheme = packageUri.getScheme();

        switch (scheme) {
            case SCHEME_PACKAGE: {
                try {
                    mPkgInfo = mPm.getPackageInfo(packageUri.getSchemeSpecificPart(),
                            PackageManager.GET_PERMISSIONS
                                    | PackageManager.MATCH_UNINSTALLED_PACKAGES);
                } catch (NameNotFoundException e) {
                }
                if (mPkgInfo == null) {
                    Log.w(TAG, "Requested package " + packageUri.getScheme()
                            + " not available. Discontinuing installation");
                    showDialogInner(DLG_PACKAGE_ERROR);
                    setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                    return false;
                }
                mAppSnippet = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo),
                        mPm.getApplicationIcon(mPkgInfo.applicationInfo));
            } break;

            case ContentResolver.SCHEME_FILE: {
                File sourceFile = new File(packageUri.getPath());
                PackageParser.Package parsed = PackageUtil.getPackageInfo(this, sourceFile);

                // Check for parse errors
                if (parsed == null) {
                    Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
                    showDialogInner(DLG_PACKAGE_ERROR);
                    setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                    return false;
                }
                mPkgInfo = PackageParser.generatePackageInfo(parsed, null,
                        PackageManager.GET_PERMISSIONS, 0, 0, null,
                        new PackageUserState());
                mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
            } break;

            default: {
                throw new IllegalArgumentException("Unexpected URI scheme " + packageUri);
            }
        }

        return true;
    }

從前面幾個Activity的處理邏輯中我們可以發現,直接通過ACTION_CONFIRM_INSTALL跳轉到PackageInstallerActivity的Uri的Scheme會被轉換成file類型;原來Scheme類型為content的在InstallStaging的StagingAsyncTask處理完畢后其Uri的Scheme也會轉成file;只有原來Scheme類型為package的是從InstallStart直接跳轉到PackageInstallerActivity的,其Scheme才是package,所以最終的Scheme只剩下packagefile,而processPackageUri()方法也只會解析這兩種uri:
1.package協議開頭的uri:這種uri會通過PackageManager的getPackageInfo()通過getSchemeSpecificPart獲取對應apk文件對應的PackageInfo信息
2.file協議開頭的uri,則使用PackageParser的generatePackageInfo()方法獲取apk文件中的package信息。最終實現mPkgInfo的屬性初始化。
3.這個方法里會引入PackageManager和PackageParser這2個類,后續會講訴這倆類和PMS之間的關系,暫不多表。

我們看一下PackageParser的generatePackageInfo()方法:

    @UnsupportedAppUsage
    public static PackageInfo generatePackageInfo(PackageParser.Package p,
            int gids[], int flags, long firstInstallTime, long lastUpdateTime,
            Set<String> grantedPermissions, PackageUserState state, int userId) {
        if (!checkUseInstalledOrHidden(flags, state, p.applicationInfo) || !p.isMatch(flags)) {
            return null;
        }
        PackageInfo pi = new PackageInfo();
        pi.packageName = p.packageName;
        pi.splitNames = p.splitNames;
        pi.versionCode = p.mVersionCode;
        pi.versionCodeMajor = p.mVersionCodeMajor;
        pi.baseRevisionCode = p.baseRevisionCode;
        pi.splitRevisionCodes = p.splitRevisionCodes;
        pi.versionName = p.mVersionName;
        pi.sharedUserId = p.mSharedUserId;
        pi.sharedUserLabel = p.mSharedUserLabel;
        pi.applicationInfo = generateApplicationInfo(p, flags, state, userId);
        pi.installLocation = p.installLocation;
        pi.isStub = p.isStub;
        pi.coreApp = p.coreApp;
        if ((pi.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0
                || (pi.applicationInfo.flags&ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
            pi.requiredForAllUsers = p.mRequiredForAllUsers;
        }
        pi.restrictedAccountType = p.mRestrictedAccountType;
        pi.requiredAccountType = p.mRequiredAccountType;
        pi.overlayTarget = p.mOverlayTarget;
        pi.targetOverlayableName = p.mOverlayTargetName;
        pi.overlayCategory = p.mOverlayCategory;
        pi.overlayPriority = p.mOverlayPriority;
        pi.mOverlayIsStatic = p.mOverlayIsStatic;
        pi.compileSdkVersion = p.mCompileSdkVersion;
        pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename;
        pi.firstInstallTime = firstInstallTime;
        pi.lastUpdateTime = lastUpdateTime;
        if ((flags&PackageManager.GET_GIDS) != 0) {
            pi.gids = gids;
        }
        if ((flags&PackageManager.GET_CONFIGURATIONS) != 0) {
            int N = p.configPreferences != null ? p.configPreferences.size() : 0;
            if (N > 0) {
                pi.configPreferences = new ConfigurationInfo[N];
                p.configPreferences.toArray(pi.configPreferences);
            }
            N = p.reqFeatures != null ? p.reqFeatures.size() : 0;
            if (N > 0) {
                pi.reqFeatures = new FeatureInfo[N];
                p.reqFeatures.toArray(pi.reqFeatures);
            }
            N = p.featureGroups != null ? p.featureGroups.size() : 0;
            if (N > 0) {
                pi.featureGroups = new FeatureGroupInfo[N];
                p.featureGroups.toArray(pi.featureGroups);
            }
        }
        if ((flags & PackageManager.GET_ACTIVITIES) != 0) {
            final int N = p.activities.size();
            if (N > 0) {
                int num = 0;
                final ActivityInfo[] res = new ActivityInfo[N];
                for (int i = 0; i < N; i++) {
                    final Activity a = p.activities.get(i);
                    if (state.isMatch(a.info, flags)) {
                        if (PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals(a.className)) {
                            continue;
                        }
                        res[num++] = generateActivityInfo(a, flags, state, userId);
                    }
                }
                pi.activities = ArrayUtils.trimToSize(res, num);
            }
        }
        if ((flags & PackageManager.GET_RECEIVERS) != 0) {
            final int N = p.receivers.size();
            if (N > 0) {
                int num = 0;
                final ActivityInfo[] res = new ActivityInfo[N];
                for (int i = 0; i < N; i++) {
                    final Activity a = p.receivers.get(i);
                    if (state.isMatch(a.info, flags)) {
                        res[num++] = generateActivityInfo(a, flags, state, userId);
                    }
                }
                pi.receivers = ArrayUtils.trimToSize(res, num);
            }
        }
        if ((flags & PackageManager.GET_SERVICES) != 0) {
            final int N = p.services.size();
            if (N > 0) {
                int num = 0;
                final ServiceInfo[] res = new ServiceInfo[N];
                for (int i = 0; i < N; i++) {
                    final Service s = p.services.get(i);
                    if (state.isMatch(s.info, flags)) {
                        res[num++] = generateServiceInfo(s, flags, state, userId);
                    }
                }
                pi.services = ArrayUtils.trimToSize(res, num);
            }
        }
        if ((flags & PackageManager.GET_PROVIDERS) != 0) {
            final int N = p.providers.size();
            if (N > 0) {
                int num = 0;
                final ProviderInfo[] res = new ProviderInfo[N];
                for (int i = 0; i < N; i++) {
                    final Provider pr = p.providers.get(i);
                    if (state.isMatch(pr.info, flags)) {
                        res[num++] = generateProviderInfo(pr, flags, state, userId);
                    }
                }
                pi.providers = ArrayUtils.trimToSize(res, num);
            }
        }
        if ((flags&PackageManager.GET_INSTRUMENTATION) != 0) {
            int N = p.instrumentation.size();
            if (N > 0) {
                pi.instrumentation = new InstrumentationInfo[N];
                for (int i=0; i<N; i++) {
                    pi.instrumentation[i] = generateInstrumentationInfo(
                            p.instrumentation.get(i), flags);
                }
            }
        }
        if ((flags&PackageManager.GET_PERMISSIONS) != 0) {
            int N = p.permissions.size();
            if (N > 0) {
                pi.permissions = new PermissionInfo[N];
                for (int i=0; i<N; i++) {
                    pi.permissions[i] = generatePermissionInfo(p.permissions.get(i), flags);
                }
            }
            N = p.requestedPermissions.size();
            if (N > 0) {
                pi.requestedPermissions = new String[N];
                pi.requestedPermissionsFlags = new int[N];
                for (int i=0; i<N; i++) {
                    final String perm = p.requestedPermissions.get(i);
                    pi.requestedPermissions[i] = perm;
                    // The notion of required permissions is deprecated but for compatibility.
                    pi.requestedPermissionsFlags[i] |= PackageInfo.REQUESTED_PERMISSION_REQUIRED;
                    if (grantedPermissions != null && grantedPermissions.contains(perm)) {
                        pi.requestedPermissionsFlags[i] |= PackageInfo.REQUESTED_PERMISSION_GRANTED;
                    }
                }
            }
        }
        // deprecated method of getting signing certificates
        if ((flags&PackageManager.GET_SIGNATURES) != 0) {
            if (p.mSigningDetails.hasPastSigningCertificates()) {
                // Package has included signing certificate rotation information.  Return the oldest
                // cert so that programmatic checks keep working even if unaware of key rotation.
                pi.signatures = new Signature[1];
                pi.signatures[0] = p.mSigningDetails.pastSigningCertificates[0];
            } else if (p.mSigningDetails.hasSignatures()) {
                // otherwise keep old behavior
                int numberOfSigs = p.mSigningDetails.signatures.length;
                pi.signatures = new Signature[numberOfSigs];
                System.arraycopy(p.mSigningDetails.signatures, 0, pi.signatures, 0, numberOfSigs);
            }
        }

        // replacement for GET_SIGNATURES
        if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
            if (p.mSigningDetails != SigningDetails.UNKNOWN) {
                // only return a valid SigningInfo if there is signing information to report
                pi.signingInfo = new SigningInfo(p.mSigningDetails);
            } else {
                pi.signingInfo = null;
            }
        }
        return pi;
    }

1.這個方法看上去又臭又長,不做詳細分析,其核心只是解析了四大組件的內容以及權限相關的標簽,拿到了apk包所有運行需要的基礎信息。
2.這個方法我們要留意2個類:PackageInfo和PackageParser.Package,后續我們會做分析。

bindUi()方法源碼:

    private void bindUi() {
        mAlert.setIcon(mAppSnippet.icon);
        mAlert.setTitle(mAppSnippet.label);
        mAlert.setView(R.layout.install_content_view);
        mAlert.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.install),
                (ignored, ignored2) -> {
                    if (mOk.isEnabled()) {
                        if (mSessionId != -1) {
                            mInstaller.setPermissionsResult(mSessionId, true);
                            finish();
                        } else {
                            startInstall();
                        }
                    }
                }, null);
        mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
                (ignored, ignored2) -> {
                    // Cancel and finish
                    setResult(RESULT_CANCELED);
                    if (mSessionId != -1) {
                        mInstaller.setPermissionsResult(mSessionId, false);
                    }
                    finish();
                }, null);
        setupAlert();

        mOk = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
        mOk.setEnabled(false);
    }

該方法是對安裝界面UI的初始化,默認會把“安裝”按鈕(mOK)隱藏掉,點擊BUTTON_POSITIVE會執行startInstall(),待我們分析完onCreate()里所有方法后再回過頭來看一下這個方法。

我們再來看onCreate()里的最后一個方法:checkIfAllowedAndInitiateInstall()

    /**
     * Check if it is allowed to install the package and initiate install if allowed. If not allowed
     * show the appropriate dialog.
     */
    private void checkIfAllowedAndInitiateInstall() {
        // Check for install apps user restriction first.
        final int installAppsRestrictionSource = mUserManager.getUserRestrictionSource(
                UserManager.DISALLOW_INSTALL_APPS, Process.myUserHandle());
        //允許安裝未知來源的文件判斷
        if ((installAppsRestrictionSource & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
            showDialogInner(DLG_INSTALL_APPS_RESTRICTED_FOR_USER);
            return;
        } else if (installAppsRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
            startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
            finish();
            return;
        }

        if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
            //核心代碼
            initiateInstall();
        } else {
            // Check for unknown sources restrictions.
            final int unknownSourcesRestrictionSource = mUserManager.getUserRestrictionSource(
                    UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, Process.myUserHandle());
            final int unknownSourcesGlobalRestrictionSource = mUserManager.getUserRestrictionSource(
                    UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, Process.myUserHandle());
            final int systemRestriction = UserManager.RESTRICTION_SOURCE_SYSTEM
                    & (unknownSourcesRestrictionSource | unknownSourcesGlobalRestrictionSource);
            if (systemRestriction != 0) {
                showDialogInner(DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER);
            } else if (unknownSourcesRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
                startAdminSupportDetailsActivity(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
            } else if (unknownSourcesGlobalRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
                startAdminSupportDetailsActivity(
                        UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY);
            } else {
                handleUnknownSources();
            }
        }
    }

繼續看其核心方法:initiateInstall()

    private void initiateInstall() {
        String pkgName = mPkgInfo.packageName;
        // Check if there is already a package on the device with this name
        // but it has been renamed to something else.
        String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
        if (oldName != null && oldName.length > 0 && oldName[0] != null) {
            pkgName = oldName[0];
            mPkgInfo.packageName = pkgName;
            mPkgInfo.applicationInfo.packageName = pkgName;
        }
        // Check if package is already installed. display confirmation dialog if replacing pkg
        try {
            // This is a little convoluted because we want to get all uninstalled
            // apps, but this may include apps with just data, and if it is just
            // data we still want to count it as "installed".
            mAppInfo = mPm.getApplicationInfo(pkgName,
                    PackageManager.MATCH_UNINSTALLED_PACKAGES);
            if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
                mAppInfo = null;
            }
        } catch (NameNotFoundException e) {
            mAppInfo = null;
        }

        startInstallConfirm();
    }

通過PackageManager的getApplicationInfo方法,獲取當前Android系統中是否已經安裝了當前的app,如果能找到mAppInfo對象,說明是安裝了,調用startInstallConfirm刷新按鈕的顯示的是更新,否則就是沒有安裝按鈕展示的是安裝。需要注意的是這里又用到了PackageManager這個類,大家請多留意。

繼續看方法startInstallConfirm():

    private void startInstallConfirm() {
        View viewToEnable;

        if (mAppInfo != null) {
            viewToEnable = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
                    ? requireViewById(R.id.install_confirm_question_update_system)
                    : requireViewById(R.id.install_confirm_question_update);
        } else {
            // This is a new application with no permissions.
            viewToEnable = requireViewById(R.id.install_confirm_question);
        }

        viewToEnable.setVisibility(View.VISIBLE);

        mEnableOk = true;
        mOk.setEnabled(true);
    }

該方法很簡單,其實就是把上文中bindUi()方法里隱藏的mOK按鈕展示出來,用來點擊進行更新/安裝,所以我們可以回頭看一下bindUi()的"mOK"按鈕點擊執行的方法startInstall()

    private void startInstall() {
        // Start subactivity to actually install the application
        Intent newIntent = new Intent();
        newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
                mPkgInfo.applicationInfo);
        newIntent.setData(mPackageURI);
        newIntent.setClass(this, InstallInstalling.class);
        String installerPackageName = getIntent().getStringExtra(
                Intent.EXTRA_INSTALLER_PACKAGE_NAME);
        if (mOriginatingURI != null) {
            newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
        }
        if (mReferrerURI != null) {
            newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
        }
        if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
            newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
        }
        if (installerPackageName != null) {
            newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
                    installerPackageName);
        }
        if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
            newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
        }
        newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);
        startActivity(newIntent);
        finish();
    }

這個方法的核心就是把mPackageURI這個字段放入Intent,然后啟動InstallInstalling這個Activity。

后續流程請看Android APK安裝流程(2)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容