PackageManagerService之应用安装流程分析

PMS是Android系统服务中的包管理机制,服务于APK的完整生命周期,主要包括APK的解析,安装,运行,卸载。

PMS整个生命周期比较庞大,可以先从安装和卸载入手,看PMS是如何工作。

APK的安装

下面这段代码估计大家都很熟悉,就是用做在应用中做自更新或者安装其他应用使用

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  Uri apkUri = FileProvider.getUriForFile(context, UIUtils.getContext().getPackageName(), file);
  intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
  intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} else {
  intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}

InstallStart

application/vnd.android.package-archive
上面这段主要做隐试匹配,会匹配到 com.android.packageInstaller进程中InstallStart(InstallStart是一个Activity)

<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>

InstallStart其实也没干什么正事,一般来说就三点

  1. 对TargetSDKVersion进行校验
  2. Manifest.permission.REQUEST_INSTALL_PACKAGES进行校验
  3. 对传递进来的uri进行校验,如果以Schema以Content开头,就跳转到InstallStaging中。
    断点看一下吧
packageUri

我们传递进来uri的值

content://com.android.externalstorage.documents/document/primary%3Aapp-release.apk

InstallStaging

先来看看这个Activity长什么样子


image

InstallStaging中主要逻辑在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());
    }
}

具体看下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];
            // 这里的packageUri 是  content://com.android.externalstorage.documents/document/primary%3Aapp-release.apk
            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);
              // 这里需要注意,已经将content开头的地址转换成了file开头的文件地址
                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();
            }
        }
    }

其实就是将APK文件拷贝一份,具体拷贝到哪里呢

拷贝到这里 /data/user/com.android.packageInstaller/no_backup/

拷贝完了之后跳转到DeleteStagedFileOnResult

ps 这里如果拷贝错误的话会出现一个比较熟悉界面

image0

DeleteStagedFileOnResult

这个类貌似是android 10新增的,在android9还没看到这个类,内容比较简单

public class DeleteStagedFileOnResult extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            Intent installIntent = new Intent(getIntent());
            // 跳转到PackageInstallerActivity,这个页面很熟悉了
            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();
    }
}

PackageInstallerActivity

还是先看看这个页面长什么样子

image1

PackageInstallerActivity中逻辑比较多

先看看在onCreate方法中初始化的主要变量

// 这个是向应用的进程提供功能,但是功能最终都有PMS提供,当前的进程还是处在com.android.packageInstaller中        
mPm = getPackageManager();

// AIDL接口,用于和PMS进行通信
mIpm = AppGlobals.getPackageManager();

// 动态权限检测
mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);

// 提供安装,卸载的功能
mInstaller = mPm.getPackageInstaller();

// 谁唤起了这次安装,如果是从应用安装,那么就是应用相关的信息,如果是从文件管理器点击安装,那么都是空的
mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                                     PackageInstaller.SessionParams.UID_UNKNOWN);

看一下传过来的apk文件地址


image3

file:///data/user_de/0/com.android.packageinstaller/no_backup/package5541502234249167159.apk

得到地址后,对APK文件开始解析,具体实现在processPackageUri方法中

ApkAssets apkAssets = ApkAssets.loadFromPath("/data/user_de/0/com.android.packageinstaller/no_backup/package5541502234249167159.apk")

XmlResourceParser parser = apkAssets.openXml(ANDROID_MANIFEST_FILENAME);

构造资源路径

 parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
// 这里的assets资源路径总共有两个
// ApkAssets{path=/system/framework/framework-res.apk},应用中有很多使用系统资源
// ApkAssets{path=/data/user_de/0/com.android.packageinstaller/no_backup/package4780013443826073855.apk}
 final Resources res = new Resources(assets, mMetrics, null);

最后看下解析的结果

activitys  // 数量和类名
applicationInfo 
baseCodePath
mCompileSdkVersion
mVersionName
mVersionCode
manifestPackageName
packageName
permissionGroups
permissions
use32bitAbi = false
codePath = /data/user_de/0/com.android.packageinstaller/no_backup/package4780013443826073855.apk
providers
receivers
.....

基本上得到在清单文件注册的所有信息

点击继续之后会出现确认安装界面


image4

在看看点击安装之后的逻辑

private void startInstall() {
        // Start subactivity to actually install the application
        Intent newIntent = new Intent();
        // 应用的ApplicationInfo
        newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
                mPkgInfo.applicationInfo);
        // mPackageURI = file:///data/user_de/0/com.android.packageinstaller/no_backup/package479667155487808554.apk
        newIntent.setData(mPackageURI);
       
       // 跳转到InstallInstalling中
        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();
    }

逻辑比较简单,就是将解析出来的ApplicationInfo和之前传递过来的PackageURI继续传递到InstallInstalling中.

这里应该会跟现在网上已经有的描述不一样,android 10在应用安装过程不会展示需要系统权限

InstallInstalling

这里差不多是在应用安装在com.android.packageinstaller进程的最后一步。
首先注册了注册一个接受应用安装成功或者失败的广播

try {
                    mInstallId = InstallEventReceiver
                            .addObserver(this, EventResultPersister.GENERATE_NEW_ID,
                                    this::launchFinishBasedOnResult);
                } catch (EventResultPersister.OutOfIdsException e) {
                    launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
                }

这里需要重点看一下这个广播

 @NonNull private static EventResultPersister getReceiver(@NonNull Context context) {
        synchronized (sLock) {
            if (sReceiver == null) {
                sReceiver = new EventResultPersister(
                // 这里返回了一个File 具体路径是
                   // /data/user_de/0/com.android.packageinstaller/no_backup/install_results.xml
// install_results.xml中记录的每次安装的installId,这次installId每次安装的时候递增
                        TemporaryFileManager.getInstallStateFile(context));
            }
        }
        return sReceiver;
    }

我们安装应用的时候通常会有进度呢,到现在还没看到进度的回调呢?

进度的回调肯定是PMS回调给InstallInstalling来展示的,接着往下看,果然在后面建立了于PMS通信

try {
  // 这种SessionID的系统应用和SystemServer的服务中很常见,因为系统服务要服务于所有应用,所以每个应用调用系统的服务的时候,在每个系统服务那里都会有一个sessionID的列表,标识着与各种应用的连接
              mSessionId =  getPackageManager().getPackageInstaller().createSession(params);
    } catch (IOException e) {
      launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
    }
mSessionCallback = new InstallSessionCallback();


// InstallSessionCallback这里就是PMS的进度回调
@Override
public void onProgressChanged(int sessionId, float progress) {
  if (sessionId == mSessionId) {
    ProgressBar progressBar = requireViewById(R.id.progress);
    progressBar.setMax(Integer.MAX_VALUE);
    progressBar.setProgress((int) (Integer.MAX_VALUE * progress));
  }
}

接着就是将APK传递给PMS,怎么传递呢,直接传递路径?NO

PackageInstaller.Session session;
            try {
              // 通过上一步创建的sessionID,创建一个安装的会话
                session = getPackageManager().getPackageInstaller().openSession(mSessionId);
            } catch (IOException e) {
                return null;
            }

            session.setStagingProgress(0);

            try {
                File file = new File(mPackageURI.getPath());

                try (InputStream in = new FileInputStream(file)) {
                    long sizeBytes = file.length();
                    // 将字节流写入PMS中,具体写到什么位置了呢?下文有介绍
                    try (OutputStream out = session
                            .openWrite("PackageInstaller", 0, sizeBytes)) {
                        byte[] buffer = new byte[1024 * 1024];
                        while (true) {
                            int numRead = in.read(buffer);

                            if (numRead == -1) {
                                session.fsync(out);
                                break;
                            }

                            if (isCancelled()) {
                                session.close();
                                break;
                            }

                            out.write(buffer, 0, numRead);
                            if (sizeBytes > 0) {
                                float fraction = ((float) numRead / (float) sizeBytes);
                                session.addProgress(fraction);
                            }
                        }
                    }
                }

                return session;
            } catch (IOException | SecurityException e) {
                Log.e(LOG_TAG, "Could not write package", e);

                session.close();

                return null;
            } finally {
                synchronized (this) {
                    isDone = true;
                    notifyAll();
                }
            }

APK传输给PMS之后下一步肯定是通知PMS我传递完了,你可以开始安装了,接着看下怎么通知了

Intent broadcastIntent = new Intent(BROADCAST_ACTION);
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntent.setPackage(getPackageName());

// 还记得这个mInstallId之前在哪里创建的吗,忘记了往前翻翻,这里将mInstallId也传递给了PMS,PMS拿到这个mInstallID,在应用安装完成后就可以发送一个广播,在把这个mInstallID回写,packageinstaller进程就可以单独去更新这个被安装应用的状态
broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId);

// PendingIntent 延时意图,满足某个条件的时候才触发
PendingIntent pendingIntent = PendingIntent.getBroadcast(
  InstallInstalling.this,
  mInstallId,
  broadcastIntent,
  PendingIntent.FLAG_UPDATE_CURRENT);

session.commit(pendingIntent.getIntentSender());
mCancelButton.setEnabled(false);
setFinishOnTouchOutside(false);

说了半天其实和PMS还没有半毛钱关系,只是说安装前的准备工作,收拢总结一下在com.android.packageinstaller中流程的工作

image5

PMS中的安装流程

在介绍PMS之前,先介绍PNS

接着上面IPC调用开始分析

session.commit(pendingIntent.getIntentSender());

对应的远程调用在 PackageInstallerSession中的commit方法

 @Override
    public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) {
        if (hasParentSessionId()) {
            throw new IllegalStateException(
                    "Session " + sessionId + " is a child of multi-package session "
                            + mParentSessionId +  " and may not be committed directly.");
        }
        if (!markAsCommitted(statusReceiver, forTransfer)) {
            return;
        }
        ... // 省略部分代码
        ...
       // 发送了一个MSG_COMMIT
        mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
    }

这个Handler通过Callback的方式处理消息,在SystemServer进程断点看一下


image06

handleCommit方法中终于调用PMS中的installStage方法


// 安装信息都在ActiveInstallSession中,ActiveInstallSession具体有哪些值呢
// mInstallPackageName = com.android.packageinstaller
// mInstallUid = 10048
// mPackageName = com.android.sourcecodedebug
// mStageDir = /data/app/vmdl1397850738.tmp
// .....

// 之前说过跨进程将APK写入到PMS中,其实就是写入到 /data/app/vmdl1397850738.tmp,它是个文件夹,里面有两个文件,后面的数字是随机生成的,不同的应用不一样
// /data/app/vmdl1397850738.tmp/lib   估计能猜出有啥作用
// /data/app/vmdl1397850738.tmp/base.apk
void installStage(ActiveInstallSession activeInstallSession) {
   
        final Message msg = mHandler.obtainMessage(INIT_COPY);
        final InstallParams params = new InstallParams(activeInstallSession);
        msg.obj = params;
       // 这个mHandler是PMS中的核心,下文会细说
       // 发送了一个INIT_COPY消息
        mHandler.sendMessage(msg);
    }

PackageHandler是处理PMS中的所有逻辑的核心,应用的安装,卸载都会触发到这个Handler,消息的种类太多,先接着上文的INIC_COPY消息来看

// 看着好像比较简单,直接调用了InstallParams的startCopy方法,看名字像是做了一次拷贝,其实不完全是
case INIT_COPY: {
  HandlerParams params = (HandlerParams) msg.obj;
  if (params != null) {   
    params.startCopy();                  
  }
  break;
}

InstallParams.startCopy中就两个方法调用

  1. handleStartCopy
  2. handleReturnCode

先看handleStartCopy

         /*
         *  
         * 方法很长,省略了部分代码,关键其实就做了两件事
           1 轻量解析 /data/app/vmdl1397850738.tmp/base.apk文件,将各种信息包装成PackageInfoLite
           2 确定应用的安装位置,三方应用,系统应用,InstantApp都有对应的规则
           * {@link PackageHelper#RECOMMEND_INSTALL_INTERNAL} to install on internal                    storage, 等于1
           * {@link PackageHelper#RECOMMEND_INSTALL_EXTERNAL} to install on external                 media,   等于2
           * {@link PackageHelper#RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage                 errors,  等于-1
         */
        public void handleStartCopy() {
            int ret = PackageManager.INSTALL_SUCCEEDED;

            ....
            ....
              
              
            PackageInfoLite pkgLite = null;
            pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mContext,
                    origin.resolvedPath, installFlags, packageAbiOverride);

          
            /*
             * 判断空间够不够,不光是APK文件,还有从APK解压释放的so文件
             * 
             */
            if (!origin.staged && pkgLite.recommendedInstallLocation
                    == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
                // TODO: focus freeing disk space on the target device
                final StorageManager storage = StorageManager.from(mContext);
                final long lowThreshold = storage.getStorageLowBytes(
                        Environment.getDataDirectory());

                final long sizeBytes = PackageManagerServiceUtils.calculateInstalledSize(
                        origin.resolvedPath, packageAbiOverride);
                if (sizeBytes >= 0) {
                    try {
                        mInstaller.freeCache(null, sizeBytes + lowThreshold, 0, 0);
                        pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mContext,
                                origin.resolvedPath, installFlags, packageAbiOverride);
                    } catch (InstallerException e) {
                        Slog.w(TAG, "Failed to free cache", e);
                    }
                }
            }
            if (ret == PackageManager.INSTALL_SUCCEEDED) {
                // 解析完就可以知道应用的类型,获取PMS推荐的安装目录,一般都是1 内置存储
                int loc = pkgLite.recommendedInstallLocation;
                .....
                .....
            }      
            mRet = ret;
        }

接着看handlerReturnCode

@Override
void handleReturnCode() {
  if (mVerificationCompleted && mEnableRollbackCompleted) {
    .....
      .....
      if (mRet == PackageManager.INSTALL_SUCCEEDED) {
        // copyAPK并没有开始复制,
        mRet = mArgs.copyApk();
      }
    processPendingInstall(mArgs, mRet);
  }
}

copyApk()并没有真正的复制,只是将FileInstallArgs对象中的两个成员变量复制,两个变量值都是一样的

/data/app/vmdl424643763.tmp

image07

最终的实现在processPendingInstall方法中,通过PackageHandler post一个Runnable

 private void processInstallRequestsAsync(boolean success,
            List<InstallRequest> installRequests) {
        mHandler.post(() -> {
            if (success) {
                for (InstallRequest request : installRequests) {
                    // 对安装目录做一下清理
                    request.args.doPreInstall(request.installResult.returnCode);
                }
                synchronized (mInstallLock) {
                    // 重点,安装过程中最核心的方法
                    installPackagesTracedLI(installRequests);
                }
              
                for (InstallRequest request : installRequests) {
                    // 安装完成之后的工作
                    request.args.doPostInstall(
                            request.installResult.returnCode, request.installResult.uid);
                }
            }
      
        });
    }

installPackagesTracedLi方法代码比较多,主要执行了下面四个逻辑
1 全量解析APK文件
2 校验APK签名,如果之前有安装过会和之前的APK签名做比对
3 释放apk文件中的so库,释放到/data/app/vmdl1397850738.tmp/lib**
4 对目录和文件充新命名,将/data/app/vmdl903343092.tmp重新命名为/data/app/包名//data/app/vmdl1397850738.tmp/base.apk重新命名为/data/app/包名/base.apk**

5 重新命名后的地址赋值给PackageParser.Package
6 更新mPackages和mSettings中的应用信息


最后一个目录比较熟悉吧!

这里描述的安装流程和现有的很多不一致的地方,主要是android 10的源代码在PMS安装这一块改动挺大,摒弃了PackageHandler中的MCS_BOUND消息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335