深度理解Android InstantRun原理以及源碼分析

深度理解Android InstantRun原理以及源碼分析

@Author 莫川

Instant Run官方介紹

簡單介紹一下Instant Run,它是Android Studio2.0以后新增的一個運行機制,能夠顯著減少你第二次及以后的構建和部署時間。簡單通俗的解釋就是,當你在Android Studio中改了你的代碼魏铅,Instant Run可以很快的讓你看到你修改的效果仿野。而在沒有Instant Run之前,你的一個小小的修改,都肯能需要幾十秒甚至更長的等待才能看到修改后的效果典尾。

傳統(tǒng)的代碼修改及編譯部署流程

構建整個apk → 部署app → app重啟 → 重啟Activity
而Instant Run則需要更少的時間。

Instant Run編譯和部署流程

只構建修改的部分 → 部署修改的dex或資源 → 熱部署糊探,溫部署钾埂,冷部署

熱部署

Incremental code changes are applied and reflected in the app without needing to relaunch the app or even restart the current Activity. Can be used for most simple changes within method implementations.
方法內(nèi)的簡單修改,無需重啟app和Activity

溫部署

The Activity needs to be restarted before changes can be seen and used. Typically required for changes to resources.
app無需重啟科平,但是activity需要重啟褥紫,比如資源的修改。

冷部署

The app is restarted (but still not reinstalled). Required for any structural changes such as to inheritance or method signatures.
app需要重啟瞪慧,比如繼承關系的改變或方法的簽名變化等髓考。
上述說這么多概念,估計大家對Instant Run應該有了大體的認知了弃酌。那么它的實現(xiàn)原理是什么呢氨菇?其實,在沒有看案例之前妓湘,我基本上可以猜測到Instant Run的思路查蓉,基于目前比較火的插件化框架,是比較容易理解Instant Run的榜贴。但Instant Run畢竟是Google官方的工具豌研,具有很好的借鑒意義。

Demo案例

新建一個簡單的android studio項目唬党,新建自己的MyApplication鹃共,在AndroidManifest文件中設置:

AndroidManifest
首先,我們先反編譯一下APK的構成:
使用的工具:d2j-dex2jar 和jd-gui

apk直接解壓之后的文件

里面有2個dex文件和一個instant-run.zip文件驶拱。首先分別看一下兩個dex文件的源碼:
classes.dex的反編譯之后的源碼:

classes.dex反編譯結果

里面只有一個AppInfo霜浴,保存了app的基本信息,主要包含了包名和applicationClass屯烦。
classes2.dex反編譯之后的源碼:

classes2.dex反編譯結果

我們赫然發(fā)現(xiàn)坷随,兩個dex中竟然沒有一句我們自己寫的代碼?驻龟?那么代碼在哪里呢温眉?你可能猜到,app真正的業(yè)務dex在instant-run.zip中翁狐。解壓instant-run.zip之后类溢,如下圖所示:

instant-run.zip解壓之后的文件

instant-run。
反編譯之后,我們會發(fā)現(xiàn)闯冷,我們真正的業(yè)務代碼都在這里砂心。
另外,我們再decode看一下AndroidManifest文件

反編譯之后的AndroidManifest.xml

//TODO
我們發(fā)現(xiàn)蛇耀,我們的application也被替換了辩诞,替換成了com.android.tools.fd.runtime.BootstrapApplication
看到這里,那么大體的思路纺涤,可以猜到:
1.Instant-Run代碼作為一個宿主程序译暂,將app作為資源dex加載起來,和插件化一個思路
2.那么InstantRun是怎么把業(yè)務代碼運行起來的呢撩炊?

InstantRun啟動app

首先BootstrapApplication分析外永,按照執(zhí)行順序,依次分析attachBaseContext和onCreate方法拧咳。

1.attachBaseContext方法

...
protected void attachBaseContext(Context context) {
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile)
.lastModified() : 0L;
createResources(apkModified);
setupClassLoaders(context, context.getCacheDir().getPath(),
apkModified);
}
createRealApplication();
super.attachBaseContext(context);
if (this.realApplication != null) {
try {
Method attachBaseContext = ContextWrapper.class
.getDeclaredMethod("attachBaseContext",
new Class[] { Context.class });
attachBaseContext.setAccessible(true);
attachBaseContext.invoke(this.realApplication,
new Object[] { context });
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
...

我們依次需要關注的方法有:
createResources → setupClassLoaders → createRealApplication → 調(diào)用realApplication的attachBaseContext方法

1.1.createResources

首先看createResources方法:

private void createResources(long apkModified) {
FileManager.checkInbox();
File file = FileManager.getExternalResourceFile();
this.externalResourcePath = (file != null ? file.getPath() : null);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource override is "
+ this.externalResourcePath);
}
if (file != null) {
try {
long resourceModified = file.lastModified();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource patch last modified: "
+ resourceModified);
Log.v("InstantRun", "APK last modified: " + apkModified
+ " "
+ (apkModified > resourceModified ? ">" : "<")
+ " resource patch");
}
if ((apkModified == 0L) || (resourceModified <= apkModified)) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Ignoring resource file, older than APK");
}
this.externalResourcePath = null;
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to check patch timestamps", t);
}
}
}

該方法主要是判斷資源resource.ap_是否改變伯顶,然后保存resource.ap_的路徑到externalResourcePath中

1.2.setupClassLoaders
private static void setupClassLoaders(Context context, String codeCacheDir,
long apkModified) {
List dexList = FileManager.getDexList(context, apkModified);
Class server = Server.class;
Class patcher = MonkeyPatcher.class;
if (!dexList.isEmpty()) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Bootstrapping class loader with dex list "
+ join('\n', dexList));
}
ClassLoader classLoader = BootstrapApplication.class
.getClassLoader();
String nativeLibraryPath;
try {
nativeLibraryPath = (String) classLoader.getClass()
.getMethod("getLdLibraryPath", new Class[0])
.invoke(classLoader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Native library path: "
+ nativeLibraryPath);
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to determine native library path "
+ t.getMessage());
nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
}
IncrementalClassLoader.inject(classLoader, nativeLibraryPath,
codeCacheDir, dexList);
}
}

繼續(xù)看IncrementalClassLoader.inject方法:
IncrementalClassLoader的源碼如下:

public class IncrementalClassLoader extends ClassLoader {
public static final boolean DEBUG_CLASS_LOADING = false;
private final DelegateClassLoader delegateClassLoader;
public IncrementalClassLoader(ClassLoader original,
String nativeLibraryPath, String codeCacheDir, List dexes) {
super(original.getParent());
this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath,
codeCacheDir, dexes, original);
}
public Class findClass(String className) throws ClassNotFoundException {
try {
return this.delegateClassLoader.findClass(className);
} catch (ClassNotFoundException e) {
throw e;
}
}
private static class DelegateClassLoader extends BaseDexClassLoader {
private DelegateClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
public Class findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
throw e;
}
}
}
private static DelegateClassLoader createDelegateClassLoader(
String nativeLibraryPath, String codeCacheDir, List dexes,
ClassLoader original) {
String pathBuilder = createDexPath(dexes);
return new DelegateClassLoader(pathBuilder, new File(codeCacheDir),
nativeLibraryPath, original);
}
private static String createDexPath(List dexes) {
StringBuilder pathBuilder = new StringBuilder();
boolean first = true;
for (String dex : dexes) {
if (first) {
first = false;
} else {
pathBuilder.append(File.pathSeparator);
}
pathBuilder.append(dex);
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Incremental dex path is "
+ BootstrapApplication.join('\n', dexes));
}
return pathBuilder.toString();
}
private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
try {
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(classLoader, newParent);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public static ClassLoader inject(ClassLoader classLoader,
String nativeLibraryPath, String codeCacheDir, List dexes) {
IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(
classLoader, nativeLibraryPath, codeCacheDir, dexes);
setParent(classLoader, incrementalClassLoader);
return incrementalClassLoader;
}
}

inject方法是用來設置classloader的父子順序的,使用IncrementalClassLoader來加載dex骆膝。由于ClassLoader的雙親委托模式祭衩,也就是委托父類加載類,父類中找不到再在本ClassLoader中查找阅签。
調(diào)用之后的效果如下圖所示:

classloader關系圖

我們可以在MyApplication中汪厨,用代碼驗證一下

@Override
public void onCreate() {
super.onCreate();
try{
Log.d(TAG,"###onCreate in myApplication");
String classLoaderName = getClassLoader().getClass().getName();
Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName);
String parentClassLoaderName = getClassLoader().getParent().getClass().getName();
Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName);
String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();
Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName);
}catch (Exception e){
e.printStackTrace();
}
}

運行結果:

...
06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader

由此,我們已經(jīng)知道了愉择,當前PathClassLoader委托IncrementalClassLoader加載dex。繼續(xù)回到BootstrapApplication的attachBaseContext方法繼續(xù)分析织中。

1.3.createRealApplication
private void createRealApplication() {
if (AppInfo.applicationClass != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"About to create real application of class name = "
+ AppInfo.applicationClass);
}
try {
Class realClass = (Class) Class
.forName(AppInfo.applicationClass);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Created delegate app class successfully : "
+ realClass + " with class loader "
+ realClass.getClassLoader());
}
Constructor constructor = realClass
.getConstructor(new Class[0]);
this.realApplication = ((Application) constructor
.newInstance(new Object[0]));
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Created real app instance successfully :"
+ this.realApplication);
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
} else {
this.realApplication = new Application();
}
}

該方法就是用classes.dex中的AppInfo類的applicationClass常量中保存的app真實的application锥涕。由上面的反編譯截圖可以知道,demo中的applicationClass就是mobctrl.net.testinstantrun.MyApplication狭吼。通過反射的方式层坠,創(chuàng)建真是的realApplication。

1.4.調(diào)用realApplication的attachBaseContext方法

代理realApplication的生命周期刁笙,通過反射調(diào)用realApplication的attachBaseContext方法破花,以當前的Context為參數(shù)。
attachBaseContext方法執(zhí)行結束之后疲吸,我們繼續(xù)往下看座每,到BootstrapApplication的onCreate方法

2.onCreate

源碼如下:

public void onCreate() {
if (!AppInfo.usingApkSplits) {
MonkeyPatcher.monkeyPatchApplication(this, this,
this.realApplication, this.externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(this,
this.externalResourcePath, null);
} else {
MonkeyPatcher.monkeyPatchApplication(this, this,
this.realApplication, null);
}
super.onCreate();
if (AppInfo.applicationId != null) {
try {
boolean foundPackage = false;
int pid = Process.myPid();
ActivityManager manager = (ActivityManager) getSystemService("activity");
List processes = manager
.getRunningAppProcesses();
boolean startServer = false;
if ((processes != null) && (processes.size() > 1)) {
for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
if (AppInfo.applicationId
.equals(processInfo.processName)) {
foundPackage = true;
if (processInfo.pid == pid) {
startServer = true;
break;
}
}
}
if ((!startServer) && (!foundPackage)) {
startServer = true;
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Multiprocess but didn't find process with package: starting server anyway");
}
}
} else {
startServer = true;
}
if (startServer) {
Server.create(AppInfo.applicationId, this);
}
} catch (Throwable t) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Failed during multi process check", t);
}
Server.create(AppInfo.applicationId, this);
}
}
if (this.realApplication != null) {
this.realApplication.onCreate();
}
}

我們依次需要關注的方法有:
monkeyPatchApplication → monkeyPatchExistingResources → Server啟動 → 調(diào)用realApplication的onCreate方法

2.1 monkeyPatchApplication

該方法的目的可以總結為:替換所有當前app的application為realApplication。

public static void monkeyPatchApplication(Context context,
Application bootstrap, Application realApplication,
String externalResourceFile) {
try {
Class activityThread = Class
.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context,
activityThread);
Field mInitialApplication = activityThread
.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication
.get(currentActivityThread);
if ((realApplication != null) && (initialApplication == bootstrap)) {
mInitialApplication.set(currentActivityThread, realApplication);
}
if (realApplication != null) {
Field mAllApplications = activityThread
.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List allApplications = (List) mAllApplications
.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
Class loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class
.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass
.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
}
for (String fieldName : new String[] { "mPackages",
"mResourcePackages" }) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry> entry : ((Map>) value)
.entrySet()) {
Object loadedApk = ((WeakReference) entry.getValue()).get();
if (loadedApk != null) {
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
if ((realApplication != null)
&& (mLoadedApk != null)) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

具體做的事情可以總結為:

1.替換ActivityThread的mInitialApplication為realApplication
2.替換mAllApplications 中所有的Application為realApplication
3.替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication摘悴。

2.2 monkeyPatchExistingResources

替換所有當前app的mAssets為newAssetManager峭梳。

public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class });
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[] { externalResourceFile })).intValue() == 0) {
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
"ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class
.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass()
.getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class
.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class
.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField(
"mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class
.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class
.getDeclaredMethod("initializeTheme",
new Class[0]);
mtm.setAccessible(true);
mtm.invoke(activity, new Object[0]);
Method mCreateTheme = AssetManager.class
.getDeclaredMethod("createTheme", new Class[0]);
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(
newAssetManager, new Object[0]);
Field mTheme = Resources.Theme.class
.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "
+ activity, e);
}
pruneResourceCaches(resources);
}
}
Collection> references;
if (Build.VERSION.SDK_INT >= 19) {
Class resourcesManagerClass = Class
.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod(
"getInstance", new Class[0]);
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null,
new Object[0]);
try {
Field fMActiveResources = resourcesManagerClass
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
ArrayMap> arrayMap = (ArrayMap) fMActiveResources
.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass
.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection) mResourceReferences
.get(resourcesManager);
}
} else {
Class activityThread = Class
.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
HashMap> map = (HashMap) fMActiveResources
.get(thread);
references = map.values();
}
for (WeakReference wr : references) {
Resources resources = (Resources) wr.get();
if (resources != null) {
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class
.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass()
.getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

改方法的目的總結為:
1.如果resource.ap_文件有改變,那么新建一個AssetManager對象newAssetManager蹂喻,然后用newAssetManager對象替換所有當前Resource葱椭、Resource.Theme的mAssets成員變量捂寿。
2.如果當前的已經(jīng)有Activity啟動了,還需要替換所有Activity中mAssets成員變量

2.3 Server啟動

判斷Server是否已經(jīng)啟動孵运,如果沒有啟動贰锁,則啟動Server

2.4 調(diào)用realApplication的onCreate方法

和1.4的目的一樣,代理realApplication的生命周期榄审。
至此罢杉,我們的app就啟動起來了。下一步就要分析大磺,Server啟動之后抡句,到底是如何進行熱部署、溫部署和冷部署了杠愧。

3.Server負責的熱部署待榔、溫部署和冷部署

首先重點關注一下Server的內(nèi)部類SocketServerReplyThread

3.1 SocketServerReplyThread

...
private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
this.mSocket = socket;
}
public void run() {
try {
DataInputStream input = new DataInputStream(
this.mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(
this.mSocket.getOutputStream());
try {
handle(input, output);
} finally {
try {
input.close();
} catch (IOException ignore) {
}
try {
output.close();
} catch (IOException ignore) {
}
}
return;
} catch (IOException e) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Fatal error receiving messages", e);
}
}
}
private void handle(DataInputStream input, DataOutputStream output)
throws IOException {
long magic = input.readLong();
if (magic != 890269988L) {
Log.w("InstantRun",
"Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
output.writeInt(4);
if (version != 4) {
Log.w("InstantRun",
"Mismatched protocol versions; app is using version 4 and tool is using version "
+ version);
} else {
int message;
for (;;) {
message = input.readInt();
switch (message) {
case 7:
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received EOF from the IDE");
}
return;
case 2:
boolean active = Restarter
.getForegroundActivity(Server.this.mApplication) != null;
output.writeBoolean(active);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Received Ping message from the IDE; returned active = "
+ active);
}
break;
case 3:
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received path-exists(" + path
+ ") from the " + "IDE; returned size="
+ size);
}
break;
case 4:
long begin = System.currentTimeMillis();
path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (Log.isLoggable("InstantRun", 2)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum)
.toString(16);
Log.v("InstantRun", "Received checksum(" + path
+ ") from the " + "IDE: took "
+ (end - begin) + "ms to compute "
+ hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received checksum(" + path
+ ") from the "
+ "IDE: returning ");
}
}
break;
case 5:
if (!authenticate(input)) {
return;
}
Activity activity = Restarter
.getForegroundActivity(Server.this.mApplication);
if (activity != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
break;
case 1:
if (!authenticate(input)) {
return;
}
List changes = ApplicationPatch
.read(input);
if (changes != null) {
boolean hasResources = Server.hasResources(changes);
int updateMode = input.readInt();
updateMode = Server.this.handlePatches(changes,
hasResources, updateMode);
boolean showToast = input.readBoolean();
output.writeBoolean(true);
Server.this.restart(updateMode, hasResources,
showToast);
}
break;
case 6:
String text = input.readUTF();
Activity foreground = Restarter
.getForegroundActivity(Server.this.mApplication);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Couldn't show toast (no activity) : "
+ text);
}
break;
}
}
}
}
...
}

socket開啟后,開始讀取數(shù)據(jù)流济,當讀到1時锐锣,獲取代碼變化的ApplicationPatch列表,然后調(diào)用handlePatches來處理代碼的變化绳瘟。

3.2 handlePatches

源碼如下:

private int handlePatches(List changes,
boolean hasResources, int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(".dex")) {
handleColdSwapPatch(change);
boolean canHotSwap = false;
for (ApplicationPatch c : changes) {
if (c.getPath().equals("classes.dex.3")) {
canHotSwap = true;
break;
}
}
if (!canHotSwap) {
updateMode = 3;
}
} else if (path.equals("classes.dex.3")) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}

1.如果后綴為“.dex”,冷部署處理handleColdSwapPatch
2.如果后綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
3.其他情況,溫部署雕憔,處理資源handleResourcePatch

handleColdSwapPatch冷部署
private static void handleColdSwapPatch(ApplicationPatch patch) {
if (patch.path.startsWith("slice-")) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received dex shard " + file);
}
}
}

把dex文件寫到私有目錄,等待整個app重啟糖声,重啟之后斤彼,使用前面提到的IncrementalClassLoader加載dex即可。

handleHotSwapPatch熱部署
private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e("InstantRun", "No file to write the code to");
return updateMode;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
this.mApplication.getCacheDir().getPath(),
nativeLibraryPath, getClass().getClassLoader());
Class aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
dexClassLoader);
try {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses", new Class[0])
.invoke(loader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v("InstantRun", "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = 3;
}
} catch (Exception e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = 3;
}
} catch (Throwable e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
updateMode = 3;
}
return updateMode;
}

將patch的dex文件寫入到臨時目錄蘸泻,然后使用DexClassLoader去加載dex琉苇。然后反射調(diào)用AppPatchesLoaderImpl類的load方法,需要說明的是悦施,AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl并扇,并實現(xiàn)了抽象方法:getPatchedClasses
如下是AbstractPatchesLoaderImpl抽象類的源碼,注意看load方法:

public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
public abstract String[] getPatchedClasses();
public boolean load() {
try {
for (String className : getPatchedClasses()) {
ClassLoader cl = getClass().getClassLoader();
Class aClass = cl.loadClass(className + "$override");
Object o = aClass.newInstance();
Class originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField("$change");
changeField.setAccessible(true);
Object previous = changeField.get(null);
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField(
"$obsolete");
if (isObsolete != null) {
isObsolete.set(null, Boolean.valueOf(true));
}
}
changeField.set(null, o);
if ((Log.logging != null)
&& (Log.logging.isLoggable(Level.FINE))) {
Log.logging.log(Level.FINE, String.format("patched %s",
new Object[] { className }));
}
}
} catch (Exception e) {
if (Log.logging != null) {
Log.logging.log(Level.SEVERE, String.format(
"Exception while patching %s",
new Object[] { "foo.bar" }), e);
}
return false;
}
return true;
}
}

由此抡诞,我們大概理清楚了InstantRun熱部署的原理:

1

在第一次構建apk時穷蛹,在每一個類中注入了一個$change的成員變量,它實現(xiàn)了IncrementalChange接口昼汗,并在每一個方法中肴熏,插入了一段類似的邏輯。

IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
... });
return;
}

就是當$change不為空的時候乔遮,執(zhí)行IncrementalChange中的方法扮超。
比如:
demo的MainActivity源代碼

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}

編譯之后的代碼為:(反編譯)

public class MainActivity extends AppCompatActivity {
public MainActivity() {
}
MainActivity(Object[] paramArrayOfObject,
InstantReloadException paramInstantReloadException) {
}
public void onCreate(Bundle paramBundle) {
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
super.onCreate(paramBundle);
setContentView(2130968601);
setSupportActionBar((Toolbar) findViewById(2131492969));
((FloatingActionButton) findViewById(2131492970))
.setOnClickListener(new View.OnClickListener() {
public void onClick(View paramAnonymousView) {
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onClick.(Landroid/view/View;)V",
new Object[] { this, paramAnonymousView });
return;
}
Snackbar.make(paramAnonymousView,
"Replace with your own action", 0)
.setAction("Action", null).show();
}
});
}
public boolean onCreateOptionsMenu(Menu paramMenu) {
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
return ((Boolean) localIncrementalChange.access$dispatch(
"onCreateOptionsMenu.(Landroid/view/Menu;)Z", new Object[] {
this, paramMenu })).booleanValue();
}
getMenuInflater().inflate(2131558400, paramMenu);
return true;
}
public boolean onOptionsItemSelected(MenuItem paramMenuItem) {
boolean bool = true;
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
bool = ((Boolean) localIncrementalChange.access$dispatch(
"onOptionsItemSelected.(Landroid/view/MenuItem;)Z",
new Object[] { this, paramMenuItem })).booleanValue();
}
while (paramMenuItem.getItemId() == 2131492993) {
return bool;
}
return super.onOptionsItemSelected(paramMenuItem);
}
}

可以看到,每個方法前,都注入了這段邏輯出刷。

2

當我們修改代碼中方法的實現(xiàn)之后璧疗,點擊InstantRun,它會生成對應的patch文件來記錄你修改的內(nèi)容馁龟。patch文件中的替換類是在所修改類名的后面追加$override崩侠,并實現(xiàn)IncrementalChange接口。
比如,以MainActivity為例
在目錄../build/intermediates/transforms/instantRun/debug/folders/4000/5下查找到.
生成了MainActivity$override類坷檩。

public class MainActivity$override implements IncrementalChange {
public MainActivity$override() {
}
public static Object init$args(Object[] var0) {
Object[] var1 = new Object[]{"android/support/v7/app/AppCompatActivity.()V"};
return var1;
}
public static void init$body(MainActivity $this) {
}
public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
Object[] var2 = new Object[]{savedInstanceState};
MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
$this.setContentView(2130968601);
Toolbar toolbar = (Toolbar)$this.findViewById(2131492969);
$this.setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton)$this.findViewById(2131492970);
Object[] var5 = new Object[]{$this};
Class[] var10002 = new Class[]{MainActivity.class};
String var10003 = "";
fab.setOnClickListener((1)((1)AndroidInstantRuntime.newForClass(var5, var10002, 1.class)));
AndroidInstantRuntime.setPrivateField($this, (TextView)$this.findViewById(2131492971), MainActivity.class, "textView");
((TextView)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "textView")).setText("myHello");
}
public static boolean onCreateOptionsMenu(MainActivity $this, Menu menu) {
$this.getMenuInflater().inflate(2131558400, menu);
return true;
}
public static boolean onOptionsItemSelected(MainActivity $this, MenuItem item) {
int id = item.getItemId();
if(id == 2131492993) {
return true;
} else {
Object[] var3 = new Object[]{item};
return ((Boolean)MainActivity.access$super($this, "onOptionsItemSelected.(Landroid/view/MenuItem;)Z", var3)).booleanValue();
}
}
public Object access$dispatch(String var1, Object... var2) {
switch(var1.hashCode()) {
case -1635453101:
return new Boolean(onCreateOptionsMenu((MainActivity)var2[0], (Menu)var2[1]));
case -1630101479:
return init$args((Object[])var2[0]);
case -641568046:
onCreate((MainActivity)var2[0], (Bundle)var2[1]);
return null;
case -604658433:
init$body((MainActivity)var2[0]);
return null;
case 1893326613:
return new Boolean(onOptionsItemSelected((MainActivity)var2[0], (MenuItem)var2[1]));
default:
throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var1, Integer.valueOf(var1.hashCode()), "mobctrl/net/testinstantrun/MainActivity"}));
}
}
3

生成AppPatchesLoaderImpl類却音,繼承自AbstractPatchesLoaderImpl,并實現(xiàn)getPatchedClasses方法矢炼,來記錄哪些類被修改了系瓢。
比如,仍然在目錄../build/intermediates/transforms/instantRun/debug/folders/4000/5下查找AppPatchesLoaderImpl.class

public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
public AppPatchesLoaderImpl() {
}
public String[] getPatchedClasses() {
return new String[]{"android.support.design.R$id", "mobctrl.net.testinstantrun.MainActivity$1", "mobctrl.net.testinstantrun.R$id", "mobctrl.net.testinstantrun.MainActivity", "android.support.v7.appcompat.R$id"};
}
}
4

調(diào)用load方法之后句灌,根據(jù)getPatchedClasses返回的修改過的類的列表夷陋,去加載對應的$override類,然后把原有類的$change設置為對應的實現(xiàn)了IncrementalChange接口的$override類胰锌。

然后等待restart之后生效

handleResourcePatch
private static int handleResourcePatch(int updateMode,
ApplicationPatch patch, String path) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
updateMode = Math.max(updateMode, 2);
return updateMode;
}

將資源的patch寫入到私有目錄骗绕,等到restart之后生效.

restart

根據(jù)不同的InstantRun的updateMode模式,進行重啟资昧,使上述的3中部署模式生效酬土!

private void restart(int updateMode, boolean incrementalResources,
boolean toast) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Finished loading changes; update mode ="
+ updateMode);
}
if ((updateMode == 0) || (updateMode == 1)) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Applying incremental code without restart");
}
if (toast) {
Activity foreground = Restarter
.getForegroundActivity(this.mApplication);
if (foreground != null) {
Restarter.showToast(foreground,
"Applied code changes without activity restart");
} else if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Couldn't show toast: no activity found");
}
}
return;
}
List activities = Restarter.getActivities(this.mApplication,
false);
if ((incrementalResources) && (updateMode == 2)) {
File file = FileManager.getExternalResourceFile();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "About to update resource file=" + file
+ ", activities=" + activities);
}
if (file != null) {
String resources = file.getPath();
MonkeyPatcher.monkeyPatchApplication(this.mApplication, null,
null, resources);
MonkeyPatcher.monkeyPatchExistingResources(this.mApplication,
resources, activities);
} else {
Log.e("InstantRun", "No resource file found to apply");
updateMode = 3;
}
}
Activity activity = Restarter.getForegroundActivity(this.mApplication);
if (updateMode == 2) {
if (activity != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Restarting activity only!");
}
boolean handledRestart = false;
try {
Method method = activity.getClass().getMethod(
"onHandleCodeChange", new Class[] { Long.TYPE });
Object result = method.invoke(activity,
new Object[] { Long.valueOf(0L) });
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Activity " + activity
+ " provided manual restart method; return "
+ result);
}
if (Boolean.TRUE.equals(result)) {
handledRestart = true;
if (toast) {
Restarter.showToast(activity, "Applied changes");
}
}
} catch (Throwable ignore) {
}
if (!handledRestart) {
if (toast) {
Restarter.showToast(activity,
"Applied changes, restarted activity");
}
Restarter.restartActivityOnUiThread(activity);
}
return;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"No activity found, falling through to do a full app restart");
}
updateMode = 3;
}
if (updateMode != 3) {
if (Log.isLoggable("InstantRun", 6)) {
Log.e("InstantRun", "Unexpected update mode: " + updateMode);
}
return;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Waiting for app to be killed and restarted by the IDE...");
}
}

總體總結

總結起來,做了一下幾件事:

第一次編譯apk:

1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
2.替換AndroidManifest.xml中的application配置
3.使用asm工具格带,在每個類中添加$change撤缴,在每個方法前加邏輯
4.把源代碼編譯成dex,然后存放到壓縮包instant-run.zip中

app運行期:

1.獲取更改后資源resource.ap_的路徑
2.設置ClassLoader叽唱。setupClassLoader:
使用IncrementalClassLoader加載apk的代碼腹泌,將原有的BootClassLoader → PathClassLoader改為BootClassLoader → IncrementalClassLoader → PathClassLoader繼承關系。
3.createRealApplication:
創(chuàng)建apk真實的application
4.monkeyPatchApplication
反射替換ActivityThread中的各種Application成員變量
5.monkeyPatchExistingResource
反射替換所有存在的AssetManager對象
6.調(diào)用realApplication的onCreate方法
7.啟動Server尔觉,Socket接收patch列表

有代碼修改時

1.生成對應的$override類
2.生成AppPatchesLoaderImpl類,記錄修改的類列表
3.打包成patch芥吟,通過socket傳遞給app
4.app的server接收到patch之后侦铜,分別按照handleColdSwapPatch、handleHotSwapPatch钟鸵、handleResourcePatch等待對patch進行處理
5.restart使patch生效

Instant Run的借鑒意義

Android插件化框架改進

Android熱修復方案

app加殼

<未完待續(xù)>

InstantRun源碼

我自己通過jd-gui反編譯獲取的钉稍,可以參考:https://github.com/nuptboyzhb/AndroidInstantRun

參考博文

[1].Instant Run: How Does it Work?!
[2].Instant Run工作原理及用法
[3].Instant Run: An Android Tool Time Deep Dive

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市棺耍,隨后出現(xiàn)的幾起案子贡未,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俊卤,死亡現(xiàn)場離奇詭異嫩挤,居然都是意外死亡,警方通過查閱死者的電腦和手機消恍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門岂昭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狠怨,你說我怎么就攤上這事约啊。” “怎么了佣赖?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵恰矩,是天一觀的道長。 經(jīng)常有香客問我憎蛤,道長外傅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任蹂午,我火速辦了婚禮栏豺,結果婚禮上,老公的妹妹穿的比我還像新娘豆胸。我一直安慰自己奥洼,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布晚胡。 她就那樣靜靜地躺著灵奖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪估盘。 梳的紋絲不亂的頭發(fā)上瓷患,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音遣妥,去河邊找鬼擅编。 笑死,一個胖子當著我的面吹牛箫踩,可吹牛的內(nèi)容都是我干的爱态。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼境钟,長吁一口氣:“原來是場噩夢啊……” “哼锦担!你這毒婦竟也來了?” 一聲冷哼從身側響起慨削,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤洞渔,失蹤者是張志新(化名)和其女友劉穎套媚,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磁椒,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡堤瘤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了衷快。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宙橱。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蘸拔,靈堂內(nèi)的尸體忽然破棺而出师郑,到底是詐尸還是另有隱情,我是刑警寧澤调窍,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布宝冕,位于F島的核電站,受9級特大地震影響邓萨,放射性物質(zhì)發(fā)生泄漏地梨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一缔恳、第九天 我趴在偏房一處隱蔽的房頂上張望宝剖。 院中可真熱鬧,春花似錦歉甚、人聲如沸万细。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赖钞。三九已至,卻和暖如春聘裁,著一層夾襖步出監(jiān)牢的瞬間雪营,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工衡便, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留献起,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓镣陕,卻偏偏與公主長得像征唬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子茁彭,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容