Amigo 0.4.4 源码解读

热修复的框架有很多,原理大同小异,从大方面区分,有从Native着手的AndFix,其他的都是得益于Android支持的MultiDex,围绕ClassLoader,从细的方面区分,就花样繁多了,可以hook的地方很多,有参考instant run方案的Robust,有整个替换ClassLoader的Amigo ,既有修改dexElements元素顺序又有整个替换ClassLoader的Tinker等等

Amigo的实现是比较简单粗暴的,其代码阅读难度不会太大,它不仅可以热修复,也能实现版本更新。

本文主要分为5部分:概述、释放APK、替换修复(包括替换 Service 的过程)、替换 Activity 的过程、局限

一、概述

拿到新的APK,怎么用? --> 拷贝其dex、so、优化dex
上面那些步骤太耗时,怎么办? --> 第一种选择:直接在后台开个线程慢慢处理,下次打开App就能生效了。第二个选择:等不及想立即生效,强制重启App,然后在Application的onCreate里,在新进程里打开一个loading的Activity,在这个Activity里开个线程去处理,处理完毕后退出这个进程
还要关注的问题有:支持替换Application吗?支持res更新吗?如何支持新增Service?如何支持新增Activity?如何支持新增Receiver?支持新增Provider吗?

二、释放APK

让补丁包生效的方法,看wiki上的说法

补丁包生效有两种方式可以选择:
稍后生效补丁包
如果不想立即生效而是用户第二次打开App 时才打入补丁包,第二次打开时就会自动生效。可以通过这个方法
Amigo.workLater(context, apkFile);
立即生效补丁包
如果想要补丁包立即生效,调用以下两个方法之一,App 会立即重启,并且打入补丁包。
Amigo.work(context, apkFile);

1、Amigo.workLater

public static void workLater(Context context, File patchFile) {
    //检查patchFile是否存在、是否有读权限、是否有新增permission、签名是否和原APK一致
    checkPatchApk(context, patchFile);
    String patchChecksum = CrcUtils.getCrc(patchFile);
    if (!PatchApks.getInstance(context).exists(patchChecksum)) {
        //将patchFile拷贝到/data/data/{packageName}/files/amigo/{checksum}/patch.apk
        copyFile(patchFile, PatchApks.getInstance(context).patchFile(patchChecksum));
    }
    //见3
    AmigoService.start(context, patchChecksum, true);
}

2、Amigo.work

public static void work(Context context, File patchFile) {
    checkPatchApk(context, patchFile);
    String patchChecksum = CrcUtils.getCrc(patchFile);
    if (!PatchApks.getInstance(context).exists(patchChecksum)) {
        copyFile(patchFile, PatchApks.getInstance(context).patchFile(patchChecksum));
    }
    //跨进程SharedPreferences,WORKING_PATCH_APK_CHECKSUM有值表示存在补丁
    context.getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS)
            .edit()
            .putString(Amigo.WORKING_PATCH_APK_CHECKSUM, patchChecksum)
            .commit();
    AmigoService.start(context, patchChecksum, false);
    //杀死自身,当前是App的默认进程(主进程)
    System.exit(0);
    Process.killProcess(Process.myPid());
}

3、AmigoService

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case WHAT:
                Context context = AmigoService.this;
                if (!isMainProcessRunning(context)) {
                    Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
                    launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(launchIntent);
                    Log.e(TAG, "start launchIntent");
                    stopSelf();
                    System.exit(0);
                    Process.killProcess(Process.myPid());
                    return;
                }
                if (count++ < RETRY_TIMES) {
                    sendMessageDelayed(Message.obtain(msg), DELAY);
                }
                break;
            default:
                break;
        }
    }
};

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent != null) {
        String checksum = intent.getStringExtra(APK_CHECKSUM);
        boolean workLater = intent.getBooleanExtra(WORK_LATER, false);
        if (workLater) {
            ApkReleaser.getInstance(this).release(checksum);
        } else {
            handler.sendMessage(handler.obtainMessage(WHAT, checksum));
        }
    }
    return super.onStartCommand(intent, flags, startId);
}

public static void start(Context context, String checksum, boolean workLater) {
    Intent intent = new Intent(context, AmigoService.class);
    intent.putExtra(WORK_LATER, workLater)
            .putExtra(APK_CHECKSUM, checksum);
    context.startService(intent);
}

AmigoService是运行在另一个进程里的(amigo进程),看它的AndroidManifest.xml配置

<service android:name="me.ele.amigo.AmigoService" android:process=":amigo"/>

如果是workLater,则执行ApkReleaser.getInstance(this).release(checksum)(见6)
如果是work,则等待主进程结束,然后再startActivity(launcher),这样实现了App重启
workLater有执行release,而work却没有,所以我们猜测,重启后在Application的onCreate里会执行release

4、AndroidManifest.xml

集成了Amigo的App运行后进入的是Amigo,而不是我们自己的Application
我们写的AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.icegeneral.amigodemo">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

其实编译后的AndroidManifest.xml并不是这样的,编译后的是这个文件app/build/intermediates/manifests/full/debug/AndroidManifest.xml

<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.icegeneral.amigodemo" android:versionCode="1" android:versionName="1.0">
  <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="24"/>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" android:name="me.ele.amigo.Amigo">
    <activity android:name="org.icegeneral.amigodemo.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <service android:name="me.ele.amigo.AmigoService" android:process=":amigo"/>
    <activity android:name="me.ele.amigo.release.ApkReleaseActivity" android:alwaysRetainTaskState="false" android:excludeFromRecents="true" android:launchMode="singleTop" android:process=":amigo" android:screenOrientation="portrait"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$StandardStub" android:launchMode="standard"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub1" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub2" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub3" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub4" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub5" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub6" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub7" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleInstanceStub8" android:launchMode="singleInstance"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub1" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub2" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub3" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub4" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub5" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub6" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub7" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTaskStub8" android:launchMode="singleTask"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub1" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub2" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub3" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub4" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub5" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub6" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub7" android:launchMode="singleTop"/>
    <activity android:name="me.ele.amigo.stub.ActivityStub$SingleTopStub8" android:launchMode="singleTop"/>
    <service android:name="me.ele.amigo.stub.ServiceStub" android:exported="false"/>
    <activity android:name="android.app.Application"/> //注意这个
  </application>
</manifest>

看到application节点android:name="me.ele.amigo.Amigo",显然是Amigo的plugin修改了AndroidManifest.xml

注意最后一个节点,原来的 Application 变成一个 activity 节点,Amigo 官方对此的解释是

Amigo Plugin 做了很 hack 的一步,就是在 AndroidManifest.xml 中将原来的 application 做为一个 Activity 。我们知道 MultiDex 分包的规则中,一定会将 Activity 放到主 dex 中,Amigo Plugin 为了保证原来的 application 被替换后仍然在主 dex 中,就做了这个十分 hack 的一步。机智的少年。

5、Amigo.onCreate --> release

public class Amigo extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            ...
            String workingPatchApkChecksum = sharedPref.getString(WORKING_PATCH_APK_CHECKSUM, "");
            try {
                //如果App当前的version_code大于上一次存储在sp里的version_code,说明App升级过
                if (checkUpgrade()) {
                    throw new RuntimeException("Host app has upgrade");
                }
                // /data/data/{packageName}/files/amigo/{workingPatchApkChecksum}/patch.apk是否存在
                //workLater()和work()都会把APK拷贝到这里
                if (TextUtils.isEmpty(workingPatchApkChecksum)
                        || !patchApks.exists(workingPatchApkChecksum)) {
                    throw new RuntimeException("Patch apk doesn't exists");
                }
            } catch (RuntimeException e) {
                e.printStackTrace();
                if (ProcessUtils.isMainProcess(this)) {
                    // clear is a dangerous operation, only need to be operated by main process
                    doClear(this);
                }
                runOriginalApplication();
                return;
            }
            //如果是amigo进程
            // ensure load dex process always run host apk not patch apk
            if (ProcessUtils.isLoadDexProcess(this)) {
                runOriginalApplication();
                return;
            }
            //isPatchApkFirstRun()在这里扯犊子,workingPatchApkChecksum是从sp取出来的,结果还去判断跟sp里的是不是不一样,那结果肯定是false
            if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(workingPatchApkChecksum)) {
                runOriginalApplication();
                return;
            }

            // only release loaded apk in the main process
            runPatchApk(workingPatchApkChecksum);
        } catch (LoadPatchApkException e) {
            e.printStackTrace();
            loadPatchError = LoadPatchError.record(LoadPatchError.LOAD_ERR, e);
            try {
                runOriginalApplication();
            } catch (Throwable e2) {
                throw new RuntimeException(e2);
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

private boolean isPatchApkFirstRun(String checksum) {
    return !sharedPref.getString(WORKING_PATCH_APK_CHECKSUM, "").equals(checksum);
}
5.1 Amigo.onCreate --> runPatchApk --> release
private void runPatchApk(String checksum) throws LoadPatchApkException {
    try {
        if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
            // TODO This is workaround for now, refactor in future.
            sharedPref.edit().remove(checksum).commit();
            releasePatchApk(checksum);
        } else {
            checkDexAndSoChecksum(checksum);
        }
        ...   //后续操作,见7
    } catch (Exception e) {
        throw new LoadPatchApkException(e);
    }
}

isPatchApkFirstRun(checksum) 扯犊子,肯定等于false
AmigoDirs.getInstance(this).isOptedDexExists(checksum) 是判断 /data/data/{package_name}/code_cache/{checksum}/amigo-dexes/ 目录底下是否存在文件
Amigo.work()的话,这个目录底下肯定还没有东西(因为work()到现在只是重启了App而已,并还没有做什么),Amigo.workLater()执行后,这个目录里应该是有东西的(release见6)

5.2 Amigo.onCreate --> runPatchApk --> releasePatchApk --> release
private void releasePatchApk(String checksum) throws Exception {
    //clear previous working dir
    clearWithoutPatchApk(checksum);

    //start a new process to handle time-tense operation
    ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA);
    String layoutName = appInfo.metaData.getString("amigo_layout");
    String themeName = appInfo.metaData.getString("amigo_theme");
    int layoutId = 0;
    int themeId = 0;
    if (!TextUtils.isEmpty(layoutName)) {
        layoutId = (int) readStaticField(Class.forName(getPackageName() + ".R$layout"), layoutName);
    }
    if (!TextUtils.isEmpty(themeName)) {
        themeId = (int) readStaticField(Class.forName(getPackageName() + ".R$style"), themeName);
    }
    ApkReleaser.getInstance(this).work(checksum, layoutId, themeId);
}

获取AndroidManifest.xml里的amigo_layout和amigo_theme

public class ApkReleaser {
    ...
    public void work(String checksum, int layoutId, int themeId) {
        if (!ProcessUtils.isLoadDexProcess(context)) {
            if (!isDexOptDone(checksum)) {
                waitDexOptDone(checksum, layoutId, themeId);
            }
        }
    }

    private boolean isDexOptDone(String checksum) {
        return context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
                .getBoolean(checksum, false);
    }
    
    private void waitDexOptDone(String checksum, int layoutId, int themeId) {
        //打开ApkReleaseActivity
        new Launcher(context).checksum(checksum).layoutId(layoutId).themeId(themeId).launch();
        while (!isDexOptDone(checksum)) {
            try {
                Thread.sleep(SLEEP_DURATION);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    ...
}

打开ApkReleaseActivity并传入amigo_layout和amigo_theme(ApkReleaseActivity内部操作见5.3 )
ApkReleaseActivity内部操作完毕,才会更新sp checksum的值,这个while才会退出循环
所以Amigo.onCreate --> runPatchApk --> releasePatchApk会一直阻塞在ApkReleaser.getInstance(this).work(checksum, layoutId, themeId),直到ApkReleaseActivity内部操作完毕

5.3 ApkReleaseActivity
public class ApkReleaseActivity extends Activity {

    static final String LAYOUT_ID = "layout_id";
    static final String THEME_ID = "theme_id";
    static final String PATCH_CHECKSUM = "patch_checksum";

    private int layoutId;
    private int themeId;
    private String checksum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        overridePendingTransition(0, 0);
        layoutId = getIntent().getIntExtra(LAYOUT_ID, 0);
        themeId = getIntent().getIntExtra(THEME_ID, 0);
        checksum = getIntent().getStringExtra(PATCH_CHECKSUM);
        if (TextUtils.isEmpty(checksum)) {
            throw new RuntimeException("patch apk checksum must not be empty");
        }
        if (themeId != 0) {
            setTheme(themeId);
        }
        if (layoutId != 0) {
            setContentView(layoutId);
        }

        ApkReleaser.getInstance(this).release(checksum);
    }

    @Override
    public void onBackPressed() {
        //do nothing
    }
}

看到onBackPressed里啥也不处理,所以按回退键不会退出ApkReleaseActivity,而且ApkReleaseActivity是运行在amigo进程

<activity android:name="me.ele.amigo.release.ApkReleaseActivity" android:alwaysRetainTaskState="false" android:excludeFromRecents="true" android:launchMode="singleTop" android:process=":amigo" android:screenOrientation="portrait"/>

主进程还被阻塞在onCreate呢,因为ApkReleaseActivity处在栈顶,所以输入事件都会进入ApkReleaseActivity,并不进入主进程,所以主进程并不会ANR,而ApkReleaseActivity里的耗时操作(release)是放在子线程,所以也不会ANR

6、ApkReleaser.release

从上面分析得知,Amigo.workLater()和Amigo.work()最终都会调用ApkReleaser.getInstance(this).release(checksum)

public void release(final String checksum) {
    if (isReleasing) {
        return;
    }
    //加入线程池
    service.submit(new Runnable() {
        @Override
        public void run() {
            isReleasing = true;
            //释放Dex到/data/data/{package_name}/files/amigo/{checksum}/dexes
            DexReleaser.releaseDexes(patchApks.patchFile(checksum), amigoDirs.dexDir(checksum));
            //拷贝so文件到/data/data/{package_name}/files/amigo/{checksum}/libs,见6.1
            NativeLibraryHelperCompat.copyNativeBinaries(patchApks.patchFile(checksum), amigoDirs.libDir(checksum));
            //优化dex文件,见6.2
            dexOptimization(checksum);
        }
    });
}
6.1 NativeLibraryHelperCompat,拷贝so文件
public static final int copyNativeBinaries(File apkFile, File sharedLibraryDir) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        return copyNativeBinariesAfterL(apkFile, sharedLibraryDir);
    } else {
        return copyNativeBinariesBeforeL(apkFile, sharedLibraryDir);
    }
}

VERSION.SDK_INT<21

private static int copyNativeBinariesBeforeL(File apkFile, File sharedLibraryDir) {
    try {
        Object[] args = new Object[2];
        args[0] = apkFile;
        args[1] = sharedLibraryDir;
        return (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "copyNativeBinariesIfNeededLI", args);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return -1;
}

private static final Class nativeLibraryHelperClass() throws ClassNotFoundException {
    return Class.forName("com.android.internal.content.NativeLibraryHelper");
}

相当于

com.android.internal.content.NativeLibraryHelper.copyNativeBinariesIfNeededLI(apkFile, sharedLibraryDir)

VERSION.SDK_INT>=21
不提供方法 copyNativeBinariesIfNeededLI(file, file),换成 findSupportedAbi() 和 copyNativeBinaries()

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static int copyNativeBinariesAfterL(File apkFile, File sharedLibraryDir) {
    try {
        Object handleInstance = MethodUtils.invokeStaticMethod(handleClass(), "create", apkFile);
        if (handleInstance == null) {
            return -1;
        }

        String abi = null;

        if (isVM64()) {
            if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
                Set<String> abis = getAbisFromApk(apkFile.getAbsolutePath());
                if (abis == null || abis.isEmpty()) {
                    return 0;
                }
                int abiIndex = (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "findSupportedAbi", handleInstance, Build.SUPPORTED_64_BIT_ABIS);
                if (abiIndex >= 0) {
                    abi = Build.SUPPORTED_64_BIT_ABIS[abiIndex];
                }
            }
        } else {
            if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
                Set<String> abis = getAbisFromApk(apkFile.getAbsolutePath());
                if (abis == null || abis.isEmpty()) {
                    return 0;
                }
                int abiIndex = (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "findSupportedAbi", handleInstance, Build.SUPPORTED_32_BIT_ABIS);
                if (abiIndex >= 0) {
                    abi = Build.SUPPORTED_32_BIT_ABIS[abiIndex];
                }
            }
        }

        if (abi == null) {
            return -1;
        }

        Object[] args = new Object[3];
        args[0] = handleInstance;
        args[1] = sharedLibraryDir;
        args[2] = abi;
        return (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "copyNativeBinaries", args);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return -1;
}

private static final Class handleClass() throws ClassNotFoundException {
    return Class.forName("com.android.internal.content.NativeLibraryHelper$Handle");
}

相当于

Object handleInstance = com.android.internal.content.NativeLibraryHelper$Handle.create(apkFile)
String abi = null;
if (isVM64()) {
    int abiIndex = com.android.internal.content.NativeLibraryHelper.findSupportedAbi(handleInstance, Build.SUPPORTED_64_BIT_ABIS);
    abi = Build.SUPPORTED_64_BIT_ABIS[abiIndex];
} else {
    int abiIndex = com.android.internal.content.NativeLibraryHelper.findSupportedAbi(handleInstance, Build.SUPPORTED_32_BIT_ABIS);
    abi = Build.SUPPORTED_32_BIT_ABIS[abiIndex];
}
com.android.internal.content.NativeLibraryHelper.copyNativeBinaries(handleInstance, sharedLibraryDir, abi);
6.2 优化dex文件
private void dexOptimization(final String checksum) {
    // /data/data/{package_name}/files/amigo/{checksum}/dexes/classes*.dex
    File[] validDexes = amigoDirs.dexDir(checksum).listFiles(new FileFilter() {
        @Override
        public boolean accept(File pathname) {
            return pathname.getName().endsWith(".dex");
        }
    });

    final CountDownLatch countDownLatch = new CountDownLatch(validDexes.length);

    for (final File dex : validDexes) {
        service.submit(new Runnable() {
            @Override
            public void run() {
                long startTime = System.currentTimeMillis();
                // /data/data/{package_name}/code_cache/{checksum}/amigo-dexes/classes*.dex
                String optimizedPath = optimizedPathFor(dex, amigoDirs.dexOptDir(checksum));
                DexFile dexFile = null;
                try {
                    dexFile = DexFile.loadDex(dex.getPath(), optimizedPath, 0);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (dexFile != null) {
                        try {
                            dexFile.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
                countDownLatch.countDown();
            }
        });
    }

    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    handler.sendMessage(handler.obtainMessage(WHAT_DEX_OPT_DONE, checksum));
}

多个classes*.dex就开启多个线程进行DexFile.loadDex(sourcePathName, outputPathName, flags),源码里对于这个方法的注释

打开一个DEX文件,并提供一个文件来保存优化过的DEX数据。如果优化过的格式已存在并且是最新的,就直接使用它。如果不是,虚拟机将试图重新创建一个。该方法主要用于应用希望在通常的应用安装机制之外下载和执行DEX文件。不能在应用里直接调用该方法,而应该通过一个类装载器例如dalvik.system.DexClassLoader.
参数
sourcePathName:包含”classes.dex”的Jar或者APK文件(将来可能会扩展支持"raw DEX")
outputPathName:保存优化过的DEX数据的文件
flags:打开可选功能(目前没定义)
返回值
一个新的,或者先前已经打开的DexFile

所有classes*.dex都优化完毕就调用

handler.sendMessage(handler.obtainMessage(WHAT_DEX_OPT_DONE, checksum));

static final int DELAY_FINISH_TIME = 4000;

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case WHAT_DEX_OPT_DONE:
                isReleasing = false;
                String checksum = (String) msg.obj;
                //更新 sp checksum 的值,更新成功后,主进程就退出 while 循环,继续往下执行了
                doneDexOpt(checksum);
                // dex 和 so 的全路径名做 key,checksum 做 value 存储到 sp,下次使用时用来和真实文件做校验,检查文件是否被修改过
                saveDexAndSoChecksum(checksum);
                context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
                        .edit()
                        .putString(Amigo.WORKING_PATCH_APK_CHECKSUM, checksum)
                        .commit();
                //等待4秒,等待肯定是因为sp真正写入到文件需要时间,只是强行等待4秒未免不太友好,应该有提升空间
                handler.sendEmptyMessageDelayed(WHAT_FINISH, DELAY_FINISH_TIME);
                break;
            case WHAT_FINISH:
                System.exit(0);
                Process.killProcess(Process.myPid());
                break;
            default:
                break;
        }
    }
};

private void doneDexOpt(String checksum) {
    context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
            .edit()
            .putBoolean(checksum, true)
            .commit();
}

最后就是退出进程,这个进程当然是amigo进程,ApkReleaseActivity当然也就消失了

四、替换修复

private void runPatchApk(String checksum) throws LoadPatchApkException {
    try {
        if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
            // TODO This is workaround for now, refactor in future.
            sharedPref.edit().remove(checksum).commit();
            //一直阻塞直到释放APK完毕
            releasePatchApk(checksum);
        } else {
            //检查文件名是否和sp里存储的一致
            checkDexAndSoChecksum(checksum);
        }
        //1、替换 ClassLoader
        // .../classes.dex:.../classes2.dex:... 这样传入ClassLoader就能一次性全部加载
        String dexPathes = getDexPath(checksum);
        AmigoClassLoader amigoClassLoader = new AmigoClassLoader(dexPathes,
                AmigoDirs.getInstance(this).dexOptDir(checksum),
                AmigoDirs.getInstance(this).libDir(checksum).getAbsolutePath(),
                getRootClassLoader());
        setAPKClassLoader(amigoClassLoader);
        patchedClassLoader = amigoClassLoader;

        //2、替换 AssetManager
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = getMatchedMethod(AssetManager.class, "addAssetPath", String.class);
        addAssetPath.setAccessible(true);
        addAssetPath.invoke(assetManager, patchApks.patchPath(checksum));
        setAPKResources(assetManager);

        //3、替换 Instrumentation
        setApkInstrumentation();

        //4、替换 H
        setApkHandler();

        //5、注册广播
        dynamicRegisterReceivers(amigoClassLoader);

        sharedPref.edit().putString(WORKING_PATCH_APK_CHECKSUM, checksum).commit();
        clearOldPatches(checksum);

        //6、IActivityManagerNative Hook
        installHook(amigoClassLoader);

        //7、运行原始的 Application
        runPatchedApplication();
    } catch (Exception e) {
        throw new LoadPatchApkException(e);
    }
}

1、替换 ClassLoader

public class AmigoClassLoader extends BaseDexClassLoader {

    public AmigoClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }

}

看看 BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    ...

dexpath 为 jar 或 apk 文件目录,多个的话用冒号分隔
optimizedDirectory 为优化 dex 缓存目录
libraryPath 为包含 native lib 的目录路径
parent 为父类加载器

只要 new AmigoClassLoader(...) 就加载了全部 classes.dex,接下来就是替换

private void setAPKClassLoader(ClassLoader classLoader) throws Exception {
    writeField(getLoadedApk(), "mClassLoader", classLoader);
}

private static Object getLoadedApk() throws Exception {
    Map<String, WeakReference<Object>> mPackages = (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
    for (String s : mPackages.keySet()) {
        WeakReference wr = mPackages.get(s);
        if (wr != null && wr.get() != null) {
            return wr.get();
        }
    }
    return null;
}

先看 instance() 是什么

public class ActivityThreadCompat {

    private static Object sActivityThread;

    private static Class sClass;

    public synchronized static final Object instance() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        if (sActivityThread == null) {
            sActivityThread = MethodUtils.invokeStaticMethod(clazz(), "currentActivityThread");
        }
        return sActivityThread;
    }

    public static final Class clazz() throws ClassNotFoundException {
        if (sClass == null) {
            sClass = Class.forName("android.app.ActivityThread");
        }
        return sClass;
    }
}

所以 instance() = ActivityThread.currentActivityThread()

要知道 ActivityThread 是什么,有什么作用,得去了解 App 启动过程、系统如何管理 Activity 的生命周期

public final class ActivityThread {

    private static ActivityThread sCurrentActivityThread;

    public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }

    private void attach(boolean system) {
        sCurrentActivityThread = this;
        ...
    }

    final ArrayMap<String, WeakReference<LoadedApk>> mPackages
        = new ArrayMap<String, WeakReference<LoadedApk>>();

getLoadedApk() 最终得到的就是LoadedApk的一个实例
所以 setAPKClassLoader() 做的事情就是

final ArrayMap<String, WeakReference<LoadedApk>> mPackages
        = ActivityThread.currentActivityThread().mPackages;

LoadedApk loadedApk = null;
for (String s : mPackages.keySet()) {
    WeakReference wr = mPackages.get(s);
    if (wr != null && wr.get() != null) {
        loadedApk = wr.get();
        break;
    }
}
loadedApk.mClassLoader = amigoClassLoader;

2、替换 AssetManager

AssetManager assetManager = new AssetManager();
assetManager.addAssetPath("/data/data/{package_name}/files/amigo/{checksum}/patch.apk");
assetManager.ensureStringBlocks();
Collection<WeakReference<Resources>> references;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    if (ResourcesManager.getInstance().mActiveResources != null) {
        ArrayMap<?, WeakReference<Resources>> arrayMap = ResourcesManager.getInstance().mActiveResources;
        references = arrayMap.values();
    } else {
        references = ResourcesManager.getInstance().mResourceReferences;
    }
} else {
    HashMap<?, WeakReference<Resources>> map = ActivityThread.currentActivityThread().mActiveResources;
    references = map.values();
}

for (WeakReference<Resources> wr : references) {
    Resources resources = wr.get();
    if (resources == null) continue;
    try {
        resources.mAssets = assetManager; //可能反射调用出错
    } catch (Throwable ignore) {
        resources.mResourcesImpl.mAssets = assetManager;
    }
    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    for (WeakReference<Resources> wr : references) {
        Resources resources = wr.get();
        if (resources == null) continue;
        // Clear all the pools
        while (resources.mTypedArrayPool.acquire() != null) ;
    }
}

3、替换 Instrumentation

Instrumentation oldInstrumentation = ActivityThread.currentActivityThread().mInstrumentation;
AmigoInstrumentation amigoInstrumentation = new AmigoInstrumentation(oldInstrumentation);
ActivityThread.currentActivityThread().mInstrumentation = amigoInstrumentation;

4、替换 H

Handler handler = ActivityThread.currentActivityThread().mH;
Callback mCallback = handler.mCallback;
AmigoCallback amigoCallback = new AmigoCallback(Amigo.this, amigoClassLoader, callback);
ActivityThread.currentActivityThread().mH = amigoCallback;

5、注册广播

这里不介绍如何找出新增的Receiver,只是需要注意的是:要使用 amigoClassLoader 来加载类

for (IntentFilter filter : filters) {
    BroadcastReceiver receiver = (BroadcastReceiver) classLoader.loadClass(addedReceiver.name).newInstance();
    context.registerReceiver(receiver, filter);
}

新增的静态注册的 receiver,这里就在 Amigo 这个 Application 的 onCreate() 里动态注册

6、IActivityManagerNative Hook

先说这个步骤的作用:用来拦截替换 Service

简单说下 startService 的过程

client
ContextImpl -->
ActivityManagerNative.ActivityManagerProxy.startService() -->
server
ActivityManagerService --> ... ->
client
ActivityThread.ApplicationThread.scheduleCreateService() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleCreateService() -->
Service.onCreate() --> return -->
server
ActivityManagerService -->
client
ActivityThread.ApplicationThread.scheduleServiceArgs() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleServiceArgs() -->
Service.onStart() & Service.onStartCommand()

private void installHook(AmigoClassLoader amigoClassLoader) throws Exception {
    Class hookFactoryClazz = amigoClassLoader.loadClass(HookFactory.class.getName());
    MethodUtils.invokeStaticMethod(hookFactoryClazz, "install", this, amigoClassLoader);
}

即 HookFactory.install(Amigo.this, amigoClassLoader);

public class HookFactory {

    private static List<Hook> mHookList = new ArrayList<>(1);

    public static void install(Context context, ClassLoader cl) {
        installHook(new IActivityManagerHook(context), cl);
    }

    private static void installHook(Hook hook, ClassLoader cl) {
        try {
            hook.onInstall(cl);
            synchronized (mHookList) {
                mHookList.add(hook);
            }
        } catch (Throwable throwable) {
        }
    }

}

相当于

new IActivityManagerHook(Amigo.this).onInstall(amigoClassLoader);

代码比较多,主要就是动态代理 ActivityManagerNative,用来拦截 Service,关键类 IActivityManagerHookHandle

public class IActivityManagerHookHandle extends BaseHookHandle {

    private static final String TAG = IActivityManagerHookHandle.class.getSimpleName();

    public IActivityManagerHookHandle(Context context) {
        super(context);
    }

    @Override
    protected void init() {
        sHookedMethodHandlers.put("startService", new startService(context));
        sHookedMethodHandlers.put("stopService", new stopService(context));
        sHookedMethodHandlers.put("stopServiceToken", new stopServiceToken(context));
        sHookedMethodHandlers.put("bindService", new bindService(context));
        sHookedMethodHandlers.put("unbindService", new unbindService(context));
        sHookedMethodHandlers.put("unbindFinished", new unbindFinished(context));
        sHookedMethodHandlers.put("peekService", new peekService(context));
    }
... 

framework 的 client 向 server 发起 binder 请求时,代理拦截,在 beforeInvoke() 将 TargetService 替换成 .stub.ServiceStub,并把原来的 intent 存入 Extras(如果TargetService不是新增的,就不会进行替换)

newIntent.putExtra(AmigoInstrumentation.EXTRA_TARGET_INTENT, intent);

server 处理完回到 client,进入 ServiceStub 的生命周期方法

public class ServiceStub extends AbstractServiceStub {

}

AbstractServiceStub 里实现了 Service 大部分的方法,方法内根据

Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);

拿到原来的 Service,再调用它的生命周期方法,比如 onStart()

[AbstractServiceStub.java]

private static ServiceManager mCreator = ServiceManager.getDefault();

@Override
public void onStart(Intent intent, int startId) {
    try {
        if (intent != null) {
            if (intent.getBooleanExtra("ActionKillSelf", false)) {
                startKillSelf();
                if (!ServiceManager.getDefault().hasServiceRunning()) {
                    stopSelf(startId);
                    boolean stopService = getApplication().stopService(intent);
                } else {
                }
            } else {
                mCreator.onStart(this, intent, 0, startId);
            }
        }
    } catch (Throwable e) {
        handleException(e);
    }
    super.onStart(intent, startId);
}

[ServiceManager.java]

public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {
    Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
    if (targetIntent != null) {
        ServiceInfo targetInfo = ServiceFinder.resolveServiceInfo(context, targetIntent);
        if (targetInfo != null) {
            Service service = mNameService.get(targetInfo.name);
            if (service == null) {
                handleCreateServiceOne(targetInfo);
            }
            return handleOnStartOne(context, targetIntent, flags);
        }
    }
    return -1;
}

private int handleOnStartOne(Context context, Intent intent, int flags) throws Exception {
    ServiceInfo info = ServiceFinder.resolveServiceInfo(context, intent);
    if (info != null) {
        Service service = mNameService.get(info.name);
        if (service != null) {
            ClassLoader classLoader = getClassLoader(context);
            intent.setExtrasClassLoader(classLoader);
            Object token = findTokenByService(service);
            Integer integer = mServiceTaskIds.get(token);
            if (integer == null) {
                integer = -1;
            }
            int startId = integer + 1;
            mServiceTaskIds.put(token, startId);
            int res = service.onStartCommand(intent, flags, startId);
            QueuedWorkCompat.waitToFinish();
            return res;
        }
    }
    return -1;
}

最终还是调用 service.onStartCommand(intent, flags, startId),这个 service 就是 TargetService,在 handleCreateServiceOne() 里面初始化的

private void handleCreateServiceOne(ServiceInfo info) throws Exception {
    Object activityThread = ActivityThreadCompat.instance();
    IBinder fakeToken = new MyFakeIBinder();
    Class CreateServiceData = Class.forName(ActivityThreadCompat.clazz().getName() + "$CreateServiceData");
    Constructor init = CreateServiceData.getDeclaredConstructor();
    if (!init.isAccessible()) {
        init.setAccessible(true);
    }
    Object data = init.newInstance();

    FieldUtils.writeField(data, "token", fakeToken);
    FieldUtils.writeField(data, "info", info);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        FieldUtils.writeField(data, "compatInfo", CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
    }

    MethodUtils.invokeMethod(activityThread, "handleCreateService", data);

    Object mService = FieldUtils.readField(activityThread, "mServices");
    Service service = (Service) MethodUtils.invokeMethod(mService, "get", fakeToken);
    MethodUtils.invokeMethod(mService, "remove", fakeToken);

    mTokenServices.put(fakeToken, service);
    mNameService.put(info.name, service);
}

相当于

CreateServiceData data = new CreateServiceData();
data.token = fakeToken;
data.info = info;
data.compatInfo = ...
activityThread.handleCreateService(data);
Service service = activityThread.mServices.get(fakeToken);
activityThread.mServices.remove(fakeToken);
mNameService.put(info.name, service);

7、运行原始的 Application

private void runOriginalApplication() throws Exception {
    setAPKClassLoader(originalClassLoader);
    Class acd = originalClassLoader.loadClass("me.ele.amigo.acd");
    String applicationName = (String) readStaticField(acd, "n");
    Application application =
            (Application) originalClassLoader.loadClass(applicationName).newInstance();
    Method attach = getMatchedMethod(Application.class, "attach", Context.class);
    attach.setAccessible(true);
    attach.invoke(application, getBaseContext());
    setAPKApplication(application);
    application.onCreate();
}

把原始的 Application 的全类名保存在 me.ele.amigo.acd.n,取出来初始化,并调用 application.attach(getBaseContext()) 和 application.onCreate() 到这里就修复完毕了,并把控制权还给原始的 Application

8、小结

使用 AmigoClassLoader 代替原来的 ClassLoader 加载新的 dex,所以所有的class都被替换了,因为四大组件需要在 AndroidManifest.xml 里面注册才能通过系统的校验,所以Amigo hook 了 Instrumentation、ActivityThread.H、ActivityManagerNative,在这些地方去拦截、替换新增的 Activity 和 Service,对于新增的静态注册的 Receiver,在 Amigo 的 onCreate() 里做了动态注册

四、替换 Activity 的过程

简单说下 startActivity 的过程

client
ContextImpl --> Instrumentation.execStartActivity() -->
server
ActivityManagerService -->...-->
client
ActivityThread.ApplicationThread.scheduleLaunchActivity() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleLaunchActivity() -->
Instrumentation.callActivityOnCreate() -->
Activity.onCreate() & Activity.onStart()

1、startActivity
Intent targetIntent = new Intent(context, TargetActivity.class);
context.startActivity(targetIntent);
2、[ContextImpl.java]
mMainThread.getInstrumentation().execStartActivity(
        getOuterContext(), mMainThread.getApplicationThread(), null,
        (Activity) null, intent, -1, options);

activityThread 的 mInstrumentation 已经被替换成 AmigoInstrumentation

3、[AmigoInstrumentation.java]
execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {

    //根据 launchMode 和 activity name 选出一个合适的 ActivityStub
    //从上面 AndroidManifest.xml 看到新增注册了很多个 Activity,有:.stub.ActivityStub$StandardStub, .stub.ActivityStub$SingleInstanceStub8, .stub.ActivityStub$SingleTopStub8...)
    //比如是标准启动模式,那么TargetActivity.class就被替换成.stub.ActivityStub$StandardStub.class(如果TargetActivity不是新增的,就不会进行替换)
    //这样做是因为没有在 AndroidManifest.xml 注册的 Activity,是没办法通过 framework 服务端的校验,所以得替换成已注册的 Activity
    intent = wrapIntent(who, intent);

    oldInstrumentation.execStartActivity(...);

}

服务端处理完毕,通知 activityThread.ApplicationThread.scheduleLaunchActivity()
--> activityThread.mH.sendMessage()
activityThread.mH 也已被替换成 AmigoCallback

4、[AmigoCallback.java]
@Override
public boolean handleMessage(Message msg) {

    if (msg.what == LAUNCH_ACTIVITY) {
        //把.stub.ActivityStub$StandardStub.class 替换回 TargetActivity.class,然后再调用 mCallback.handleMessage(msg)
        // mCallback 就是原本的 activityThread.mH
        return handleLaunchActivity(msg);
    }
    if (mCallback != null) {
        return mCallback.handleMessage(msg);
    }
    return false;
}

--> activityThread.handleLaunchActivity()
--> mInstrumentation.callActivityOnCreate()

5、[AmigoInstrumentation.java]
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    try {
        Intent targetIntent = activity.getIntent();
        if (targetIntent != null) {
            ActivityInfo targetInfo = targetIntent.getParcelableExtra(EXTRA_TARGET_INFO);
            if (targetInfo != null) {
                activity.setRequestedOrientation(targetInfo.screenOrientation);

                ComponentName componentName = new ComponentName(activity, getDelegateActivityName(activity, activity.getClass().getName()));
                FieldUtils.writeField(activity, "mComponent", componentName);

                Class stubClazz = (Class) targetIntent.getSerializableExtra(EXTRA_STUB_NAME);
                if (stubClazz != null)
                    ActivityStub.onActivityCreated(stubClazz, activity, "");
            }
        }
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    if (oldInstrumentation != null) {
        oldInstrumentation.callActivityOnCreate(activity, icicle);
    } else {
        super.callActivityOnCreate(activity, icicle);
    }

}

五、局限

不支持新增 provider
不支持修改 notification 和 widget 的布局
不支持修改 launcher activity 的全类名
不支持新增 permission
除了官方说的这些局限外,IActivityManagerHookHandle.java 里面有这样一句话

TODO: we may need to support new added services in multi-process app

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

推荐阅读更多精彩内容