上一篇主要分析了Robust的使用方法,这一篇就来总结一下Robust的源码分析。

  我个人倾向于将Robust框架分为两个部分,自动插入代码和动态加载Patch。

一、Robust源码分析

  目前我的分析将Robust动态加载分为两个部分,一部分是插桩后的代码逻辑,一部分是拉取Patch的逻辑。

  我们首先来看插桩后的代码(这里面套用的是官方的代码,可能有些过时了)

  插桩前

public long getIndex() {
return ;
}

  插桩后

public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}

  我们可以看到Robust为我们的类添加了一个静态的ChangeQuickRedirect对象,我们可以看到当ChangeQuickRedirect为空时,证明此时没有补丁,走原逻辑。当它不为空时,我们可以看到它调用了PatchProxy中的isSupport方法和accessDispatch方法。我们具体来看一下PatchProxy中的这两个方法。

  

   public static boolean isSupport(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
//Robust补丁优先执行,其他功能靠后
if (changeQuickRedirect == null) {
//不执行补丁,轮询其他监听者
if (registerExtensionList == null || registerExtensionList.isEmpty()) {
return false;
}
for (RobustExtension robustExtension : registerExtensionList) {
if (robustExtension.isSupport(new RobustArguments(paramsArray, current, isStatic, methodNumber, paramsClassTypes, returnType))) {
robustExtensionThreadLocal.set(robustExtension);
return true;
}
}
return false;
}
String classMethod = getClassMethod(isStatic, methodNumber);
if (TextUtils.isEmpty(classMethod)) {
return false;
}
Object[] objects = getObjects(paramsArray, current, isStatic);
try {
return changeQuickRedirect.isSupport(classMethod, objects);
} catch (Throwable t) {
return false;
}
}

  我们可以看到第22行,它调用了changeQuickRedirect.isSupport方法,这个changeQuickRedirect便是我们注入的对象。

  我们接下来再看accessDispatch方法

  public static Object accessDispatch(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {

         if (changeQuickRedirect == null) {
RobustExtension robustExtension = robustExtensionThreadLocal.get();
robustExtensionThreadLocal.remove();
if (robustExtension != null) {
notify(robustExtension.describeSelfFunction());
return robustExtension.accessDispatch(new RobustArguments(paramsArray, current, isStatic, methodNumber, paramsClassTypes, returnType));
}
return null;
}
String classMethod = getClassMethod(isStatic, methodNumber);
if (TextUtils.isEmpty(classMethod)) {
return null;
}
notify(Constants.PATCH_EXECUTE);
Object[] objects = getObjects(paramsArray, current, isStatic);
return changeQuickRedirect.accessDispatch(classMethod, objects);

  可以看到第18行调用了changeQuickRedirect的accseeDispatch方法。

  注入后的代码我们先看到这里,我们接下来看一看,我们拉取Patch的代码

   new PatchExecutor(getApplicationContext(), new PatchManpulateImp(), new RobustCallBack() {
@Override
public void onPatchListFetched(boolean result, boolean isNet, List<Patch> patches) {
Log.e("error-hot", "打印 onPatchListFetched:" + "isNet=" + isNet );
}
@Override
public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
Log.e("error-hot", "打印 onPatchFetched:" + "result=" + result+"isNet="+isNet + "--->" + "patch=" + patch);
}
@Override
public void onPatchApplied(boolean result, Patch patch) {
Log.e("error-hot", "打印 onPatchApplied:" + "result=" + result + "--->" + "patch=" + patch);
}
@Override
public void logNotify(String log, String where) {
Log.e("error-hot", "打印 logNotify:" + "log=" + log + "--->" + "where=" + where);
}
@Override
public void exceptionNotify(Throwable throwable, String where) {
Log.e("error-hot", "打印 exceptionNotify:" + "throwable=" + throwable.toString() + "--->" + "where=" + where);
}
}).start();

  进入PatchExecutor类中看一看,我们可以发现它继承了一个线程,那么直接去run方法看一下

    @Override
public void run() {
try {
//拉取补丁列表
List<Patch> patches = fetchPatchList();
//应用补丁列表
applyPatchList(patches);
} catch (Throwable t) {
Log.e("robust", "PatchExecutor run", t);
robustCallBack.exceptionNotify(t, "class:PatchExecutor,method:run,line:36");
}
}

  可以看到run方法中做了两件事,拉取补丁列表和应用补丁列表。

  我们接着进入fetchPatchList方法

    protected List<Patch> fetchPatchList() {
return patchManipulate.fetchPatchList(context);
}

  他返回了patchManipulate的fetchPatchList方法,这个对象便是我们在初始化的时候传进来的。我们进入看一看

     @Override
protected List<Patch> fetchPatchList(Context context) {
Patch patch = new Patch();
patch.setName("test patch");
patch.setLocalPath(Environment.getExternalStorageDirectory().getPath()+
File.separator+"robust"+File.separator+"patch");
patch.setPatchesInfoImplClassFullName("com.example.tyr.testrobust.PatchesInfoImpl");
List<Patch> patches = new ArrayList<>();
patches.add(patch);
return patches;
}

  我们将这个PatchesInfoImpl拉进到列表中,那么这个PatchInfoImpl是在哪里那?我们后面再说。

  接着看applyPatchList方法

  

protected void applyPatchList(List<Patch> patches) {
if (null == patches || patches.isEmpty()) {
return;
}
Log.d("robust", " patchManipulate list size is " + patches.size());
for (Patch p : patches) {
if (p.isAppliedSuccess()) {
Log.d("robust", "p.isAppliedSuccess() skip " + p.getLocalPath());
continue;
}
if (patchManipulate.ensurePatchExist(p)) {
boolean currentPatchResult = false;
try {
currentPatchResult = patch(context, p);
} catch (Throwable t) {
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
}
if (currentPatchResult) {
//设置patch 状态为成功
p.setAppliedSuccess(true);
//统计PATCH成功率 PATCH成功
robustCallBack.onPatchApplied(true, p); } else {
//统计PATCH成功率 PATCH失败
robustCallBack.onPatchApplied(false, p);
} Log.d("robust", "patch LocalPath:" + p.getLocalPath() + ",apply result " + currentPatchResult); }
}
}

  可以看到for循环patches中的每一个patch并调用patch方法。我们接着进入patch方法。

 protected boolean patch(Context context, Patch patch) {
if (!patchManipulate.verifyPatch(context, patch)) {
robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
return false;
} DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
patch.delete(patch.getTempPath()); Class patchClass, oldClass; Class patchsInfoClass;
PatchesInfo patchesInfo = null;
try {
Log.d("robust", "PatchsInfoImpl name:" + patch.getPatchesInfoImplClassFullName());
patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
Log.d("robust", "PatchsInfoImpl ok");
} catch (Throwable t) {
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:108");
Log.e("robust", "PatchsInfoImpl failed,cause of" + t.toString());
t.printStackTrace();
} if (patchesInfo == null) {
robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
return false;
} //classes need to patch
List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
if (null == patchedClasses || patchedClasses.isEmpty()) {
robustCallBack.logNotify("patchedClasses is null or empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:122");
return false;
} for (PatchedClassInfo patchedClassInfo : patchedClasses) {
String patchedClassName = patchedClassInfo.patchedClassName;
String patchClassName = patchedClassInfo.patchClassName;
if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
continue;
}
Log.d("robust", "current path:" + patchedClassName);
try {
oldClass = classLoader.loadClass(patchedClassName.trim());
Field[] fields = oldClass.getDeclaredFields();
Log.d("robust", "oldClass :" + oldClass + " fields " + fields.length);
Field changeQuickRedirectField = null;
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
changeQuickRedirectField = field;
break;
}
}
if (changeQuickRedirectField == null) {
robustCallBack.logNotify("changeQuickRedirectField is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
Log.d("robust", "current path:" + patchedClassName + " something wrong !! can not find:ChangeQuickRedirect in" + patchClassName);
continue;
}
Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);
try {
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);
Log.d("robust", "changeQuickRedirectField set sucess " + patchClassName);
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
t.printStackTrace();
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
}
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
t.printStackTrace();
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");
}
}
Log.d("robust", "patch finished ");
return true;
}

  可以看到我们的类加载器在这里加载了我们的patch,我们接下来可以看到classloader加载了我们的PatchInfoImpl类。在这个类中继承了Robust的PatchInfo接口,这里只有一个方法

 public interface PatchesInfo {
List<PatchedClassInfo> getPatchedClassesInfo();
}  

  他拉取了我们需要修改的类的信息。

  这里面的PatchedClassInfo中保存了两个类的信息,一个是我们需要修改的类PatchedClass和修改他的类PatchClass。

  第39,40行Robust拿到了这两个类的名字。

  第48行通过反射获取了我们需要修改的类的所有field

  接下来是一个for循环获取到我们注入代码中的静态ChangeQuickRedirect对象。

  获取到对象后我们看第64行他加载了我们PatchClass的类

  接下来的65,66,67三行,我们可以看到他通过反射将我们PatchedClass即oldClass中的changeQuickRedirect字段赋值为我们的PatchClass。至于这个PatchClass是什么。我们接下来说。

  到目前为止,我们可以看到,插桩后的逻辑已经说完了,不得不说Robust的原理还是比较通俗易懂的。我们接下来回答前面的两个剩余问题,PatchInfoImpl和PatchClass在哪里。我们顺着我们的Patch.jar去寻找。反编译后得到如下列表。

  

  找到了我们的PatchesInfoImpl,而我们的PatchClass就是RobustActivityPatchControl了

  我们先来看一看PatchesInfoImpl做了什么

 import com.meituan.robust.PatchedClassInfo;
import com.meituan.robust.PatchesInfo;
import java.util.ArrayList;
import java.util.List; public class PatchesInfoImpl
implements PatchesInfo
{
public List getPatchedClassesInfo()
{
ArrayList localArrayList = new ArrayList();
localArrayList.add(new PatchedClassInfo("com.example.tyr.testrobust.RobustActivity", "com.example.tyr.testrobust.RobustActivityPatchControl"));
com.meituan.robust.utils.EnhancedRobustUtils.isThrowable = false;
return localArrayList;
}
}

  可以看到他把我们的patchedClass和patchClass加入了list中,也就是上面返回的信息。

  我们接着看我们注入的这个patchClass中的方法

 public class RobustActivityPatchControl
implements ChangeQuickRedirect
{
public static final String MATCH_ALL_PARAMETER = "(\\w*\\.)*\\w*";
private static final Map<Object, Object> keyToValueRelation = new WeakHashMap(); private static Object fixObj(Object paramObject)
{
Object localObject = paramObject;
if ((paramObject instanceof Byte))
if (((Byte)paramObject).byteValue() == 0)
break label32;
label32: for (boolean bool = true; ; bool = false)
{
localObject = new Boolean(bool);
return localObject;
}
} public Object accessDispatch(String paramString, Object[] paramArrayOfObject)
{
label134:
while (true)
try
{
if (!paramString.split(":")[2].equals("false"))
continue;
if (keyToValueRelation.get(paramArrayOfObject[(paramArrayOfObject.length - 1)]) != null)
continue;
RobustActivityPatch localRobustActivityPatch = new RobustActivityPatch(paramArrayOfObject[(paramArrayOfObject.length - 1)]);
keyToValueRelation.put(paramArrayOfObject[(paramArrayOfObject.length - 1)], null);
break label134;
if (!"12".equals(paramString.split(":")[3]))
break;
localRobustActivityPatch.onCreate((Bundle)paramArrayOfObject[0]);
return null;
localRobustActivityPatch = (RobustActivityPatch)keyToValueRelation.get(paramArrayOfObject[(paramArrayOfObject.length - 1)]);
break label134;
localRobustActivityPatch = new RobustActivityPatch(null);
continue;
}
catch (Throwable paramString)
{
paramString.printStackTrace();
return null;
}
return null;
} public Object getRealParameter(Object paramObject)
{
Object localObject = paramObject;
if ((paramObject instanceof RobustActivity))
localObject = new RobustActivityPatch(paramObject);
return localObject;
} public boolean isSupport(String paramString, Object[] paramArrayOfObject)
{
paramString = paramString.split(":")[3];
return ":12:".contains(":" + paramString + ":");
}
}

  我们可以看到它实现了Robust的ChangeQuickRedirect接口,并实现了他们的两个方法accessDispatch和isSupport的两个方法,也就是PatchProxy中调用的这两个方法。

  先说isSupport方法

  这里的isSupport方法是混淆后的,我们可以看到在PatchProxy类中,他传入了classMethod的名字和这个方法所需要的参数。校验后进行判断。

  在PatchProxy中他传入了的classMethod格式为className:methodName:isStatic:methodNumber。这里只校验了方法的number。这里是在accessDispatch中传入,目测插桩后的代码有所改动。

  继续说accessPatch方法。

  第26行校验了是否为静态方法。将参数数组传给了RobustActivityPatch这个类,并调用了它的onCreat方法,莫名的熟悉感,这个就是我们标注为Modify标签的那个类。

  我们接下来看一看RobustActivityPatch这个类

  

 import android.os.Bundle;
import android.support.v7.app.c;
import android.view.View;
import android.widget.TextView;
import com.meituan.robust.utils.EnhancedRobustUtils; public class RobustActivityPatch
{
RobustActivity originClass; public RobustActivityPatch(Object paramObject)
{
this.originClass = ((RobustActivity)paramObject);
} public static void staticRobustonCreate(RobustActivityPatch paramRobustActivityPatch, RobustActivity paramRobustActivity, Bundle paramBundle)
{
RobustActivityPatchRobustAssist.staticRobustonCreate(paramRobustActivityPatch, paramRobustActivity, paramBundle);
} public Object[] getRealParameter(Object[] paramArrayOfObject)
{
if ((paramArrayOfObject == null) || (paramArrayOfObject.length < 1))
return paramArrayOfObject;
Object[] arrayOfObject = new Object[paramArrayOfObject.length];
int i = 0;
if (i < paramArrayOfObject.length)
{
if ((paramArrayOfObject[i] instanceof Object[]))
arrayOfObject[i] = getRealParameter((Object[])paramArrayOfObject[i]);
while (true)
{
i += 1;
break;
if (paramArrayOfObject[i] == this)
{
arrayOfObject[i] = this.originClass;
continue;
}
arrayOfObject[i] = paramArrayOfObject[i];
}
}
return arrayOfObject;
} protected void onCreate(Bundle paramBundle)
{
staticRobustonCreate(this, this.originClass, paramBundle);
EnhancedRobustUtils.invokeReflectMethod("setContentView", ((RobustActivityPatch)this).originClass, getRealParameter(new Object[] { new Integer(2131296284) }), new Class[] { Integer.TYPE }, c.class);
paramBundle = (View)EnhancedRobustUtils.invokeReflectMethod("findViewById", ((RobustActivityPatch)this).originClass, getRealParameter(new Object[] { new Integer(2131165307) }), new Class[] { Integer.TYPE }, c.class);
if (paramBundle == this);
for (paramBundle = ((RobustActivityPatch)paramBundle).originClass; ; paramBundle = (TextView)paramBundle)
{
String str = (String)EnhancedRobustUtils.invokeReflectMethod("RobustPublicgetString", new RobustActivityInLinePatch(getRealParameter(new Object[] { this })[0]), getRealParameter(new Object[0]), null, null);
Object localObject = paramBundle;
if (paramBundle == this)
localObject = ((RobustActivityPatch)paramBundle).originClass;
EnhancedRobustUtils.invokeReflectMethod("setText", localObject, getRealParameter(new Object[] { str }), new Class[] { CharSequence.class }, TextView.class);
return;
}
}
}

  看到onCreate方法,第48行,这里我们可以看到他特殊处理了一下我们在RobustActivity的onCreate方法,感觉有点怪怪的,这里似乎是又执行了一边RobustActivity的OnCreate方法,而不是super.onCreate。

  ps:看了看美团官方关于super的解析,似乎是这样的,他通过调用RobustActivity的OnCreate,将class文件中的invokevirtual指令替换为invokesuper指令,从而达到super的效果,这里面还有个问题,如果这样调用会出现这样的问题

Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)

  Robust的解决方案是使这个类也继承RobustActivity的父类,我们可以看到RobustActivityPatchRobustAssist类果然继承了一个类,但是由于混淆我们看到的是他继承了一个c的类,猜测它应该就是RobustActivity的父类AppCompatActivity。

  验证一下打印dex文件

  

  看到invoke-super指令,现在可以确定了

  再看一下RobustActivityPatchRobustAssist的父类,这绝对就是android.support.v7.app.AppcompatActivity了。

  

  然后我们可以看到他执行了setContentView,findViewById这里传入的两串数字便是我们的布局和空间在R类的数字。

  然后我们可以看到它执行到了我们修改代码的地方。

  第54行它调用了RobustPublicgetString方法,又是莫名的熟悉感,,

  进入RobustActivityInLinePatch看一看。

 public class RobustActivityInLinePatch
{
RobustActivity originClass; public RobustActivityInLinePatch(Object paramObject)
{
this.originClass = ((RobustActivity)paramObject);
} private String getString()
{
return "hello robust";
} public String RobustPublicgetString()
{
return getString();
} public Object[] getRealParameter(Object[] paramArrayOfObject)
{
if ((paramArrayOfObject == null) || (paramArrayOfObject.length < 1))
return paramArrayOfObject;
Object[] arrayOfObject = new Object[paramArrayOfObject.length];
int i = 0;
if (i < paramArrayOfObject.length)
{
if ((paramArrayOfObject[i] instanceof Object[]))
arrayOfObject[i] = getRealParameter((Object[])paramArrayOfObject[i]);
while (true)
{
i += 1;
break;
if (paramArrayOfObject[i] == this)
{
arrayOfObject[i] = this.originClass;
continue;
}
arrayOfObject[i] = paramArrayOfObject[i];
}
}
return arrayOfObject;
}
}

  可以看到我们传入的getString方法出现在了这里。

二、总结

  到目前为止,Robust的逻辑算是走通了。

  目前为止,我认为Robust的核心应该算是它自动插桩的那一部分,目前暂时不涉及了,下一篇将会了解一下热修复背后的动态加载。

参考资料:

  Android中热修复框架Robust原理解析+并将框架代码从"闭源"变成"开源"(上篇)

  Android热更新方案Robust

美团热修复Robust-源码篇的更多相关文章

  1. 美团热修复Robust的踩坑之旅-使用篇

    最近需要在项目中使用热修复框架,在这里以美团的Robust为主写一篇文章总结一下学习的过程. 一直认为要学习一个框架的原理,首先需要让他跑起来,从效果反推回去,这样更容易理解. 一.美团Robust的 ...

  2. 源码篇:Flutter Provider的另一面(万字图文+插件)

    前言 阅读此文的彦祖,亦菲们,附送一枚Provider模板代码生成插件! 我为啥要写这个插件呢? 此事说来话短,我这不准备写解析Provider源码的文章,肯定要写这框架的使用样例啊,然后再哔哔源码呀 ...

  3. 深入浅出Mybatis系列(五)---TypeHandler简介及配置(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)>为大家介绍了mybatis中别名的使用,以及其源码.本篇将为大家介绍TypeH ...

  4. 深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)> 介绍了properties与environments, ...

  5. 深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇)>我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根 ...

  6. 源码篇:SDWebImage

    攀登,一步一个脚印,方能知其乐 源码篇:SDWebImage 源码来源:https://github.com/rs/SDWebImage 版本: 3.7 SDWebImage是一个开源的第三方库,它提 ...

  7. springboot2.0.3源码篇 - 自动配置的实现,发现也不是那么复杂

    前言 开心一刻 女儿: “妈妈,你这么漂亮,当年怎么嫁给了爸爸呢?” 妈妈: “当年你爸不是穷嘛!‘ 女儿: “穷你还嫁给他!” 妈妈: “那时候刚刚毕业参加工作,领导对我说,他是我的扶贫对象,我年轻 ...

  8. spring-boot-2.0.3源码篇 - pageHelper分页,绝对有值得你看的地方

    前言 开心一刻 说实话,作为一个宅男,每次被淘宝上的雄性店主追着喊亲,亲,亲,这感觉真是恶心透顶,好像被强吻一样.........更烦的是我每次为了省钱,还得用个女号,跟那些店主说:“哥哥包邮嘛么叽. ...

  9. shiro源码篇 - 疑问解答与系列总结,你值得拥有

    前言 开心一刻 小明的朋友骨折了,小明去他家里看他.他老婆很细心的为他换药,敷药,然后出去买菜.小明满脸羡慕地说:你特么真幸福啊,你老婆对你那么好!朋友哭得稀里哗啦的说:兄弟你别说了,我幸福个锤子,就 ...

随机推荐

  1. H5新增属性classList

    H5新增属性classList h5中新增了一个classList,原生js可以通过它来判断获取dom节点有无某个class. classList是html元素对象的成员,它的使用非常简单,比如 co ...

  2. ionic开发中遇到的问题

    开发调试过程中,会遇到这样的问题:同源策略请求url禁止请求. 一   网上搜的结果基本是2类: 1. 同源策略请求被阻止, 跨域问题,大家建议添加Access-Control-Allow-Origi ...

  3. java.lang.VerifyError: com/google/android/gms/measurement/internal/zzw

    android studio  com.google.android.gms:play-services 运行报错:java.lang.VerifyError: com/google/android/ ...

  4. MVP 模式简单易懂的介绍方式

    为什么用Android MVP 设计模式? 当项目越来越庞大.复杂,参与的研发人员越来越多的时候,MVP 模式 的优势就充分显示出来了. MVP 模式是 MVC 模式在 Android 上的一种变体, ...

  5. Android 自定义AlertDialog(退出提示框)

    有时候我们需要在游戏或应用中用一些符合我们样式的提示框(AlertDialog) 以下是我在开发一个小游戏中总结出来的.希望对大家有用. 先上效果图: 下面是用到的背景图或按钮的图片 经过查找资料和参 ...

  6. Flex Box 简单弹性布局

    弹性盒子模型有两种规范:早起的display:box 和后期的display:flex.它可以轻易的实现均分.浮动.居中等灵活布局,在移动端只考虑webkit内核时很实用. 一.display:box ...

  7. zookeeper - java操作

    ZKUtils.java package test; import java.io.IOException; import java.util.concurrent.CountDownLatch; i ...

  8. 一种特殊场景下的HASH JOIN的优化为NEST LOOP.

    应用场景: 有如下的SQL: select t.*, t1.ownerfrom t, t1where t.id=t1.id; 表t ,t1的数据量比较大,比如200W行.但是两张表能关联的行数却很少, ...

  9. 如何计算tomcat线程池大小?

    背景 在我们的日常开发中都涉及到使用tomcat做为服务器,但是我们该设置多大的线程池呢?以及根据什么原则来设计这个线程池呢? 接下来,我将介绍本人是怎么设计以及计算的. 目标 确定tomcat服务器 ...

  10. [翻译] WPAttributedMarkup

    WPAttributedMarkup https://github.com/nigelgrange/WPAttributedMarkup WPAttributedMarkup is a simple ...