Android 插件化原理学习 —— Hook 机制之动态代理

前言

为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Native、weex 等,而国内一线大厂基本上主要还是 Android 插件化解决大部分的更新问题,对于部分是采用 webview 或者 React Native 这种方案,而对于 Android 插件化采用的技术对于 Android Framewrok 的理解要求很高,真正实现落地的方案都还是有难度,对于非 Android Native 开发的人员更是有技术门槛。插件化可以很好的解决 Android 运行的一些问题,本文站在学习者的角度去尝试理解插件化到底解决了什么问题。

插件化框架

如下是主流的插件化框架之间的对比:

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大组件 只支持 Activity 只支持 Activity 只支持 Activity 全支持 全支持
无需在宿主 manifest 中预注册 ×
插件可以依赖宿主 ×
支持 PendingIntent × × ×
Android 特性支持 大部分 大部分 大部分 几乎全部 几乎全部
兼容性适配 一般 一般 中等
插件构建 部署 aapt Gradle 插件 Gradle 插件

代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。使用代理可以屏蔽内部实现细节,后续内部有变动对于外部调用者来说是封闭的,符合开放-封闭原则。用户可以放心地请求代理,他只关心是否能得到想要的结果。在任何使用本体的地方都可以替换成使用代理,从而实现实现和调用松耦合。

不用代理模式:

不用代理模式

使用代理模式:

使用代理模式

静态代理

例如我们有两个接口:

// Subject1.java
public interface Subject1 {
  void method1();
  void method2();
}

// Subject2.java
public interface Subject2 {
  void method1();
  void method2();
  void method3();
}

我们分别实现这两个接口:

// RealSubject1.java
public class RealSubject1 implements Subject1 {
  @Override
  public void method1() {
    Logger.i(RealSubject1.class, "我是RealSubject1的方法1");
  }

  @Override
  public void method2() {
    Logger.i(RealSubject1.class, "我是RealSubject1的方法2");
  }
}

// RealSubject2.java
public class RealSubject2 implements Subject2 {
  @Override
  public void method1() {
    Logger.i(RealSubject2.class, "我是RealSubject2的方法1");
  }

  @Override
  public void method2() {
    Logger.i(RealSubject2.class, "我是RealSubject2的方法2");
  }

  @Override
  public void method3() {
    Logger.i(RealSubject2.class, "我是RealSubject2的方法3");
  }
}

如果不使用代理模式,我们一般会直接实例化 RealSubject1 和 RealSubject2 类对象。使用代理,我们一般都需要建立一个代理类。在 Java 等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥 有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开 编译器的类型检查,代理和本体将来可以被替换使用。

/**
 * 静态代理类(为了保持行为的一致性,代理类和委托类通常会实现相同的接口)
 * ProxySubject1.java
 */
public class ProxySubject1 implements Subject1 {
  private Subject1 subject1;

  public ProxySubject1(Subject1 subject1) {
    this.subject1 = subject1;
  }

  @Override
  public void method1() {
    Logger.i(ProxySubject1.class, "我是代理,我会在执行实体方法1之前先做一些预处理的工作");
    subject1.method1();
  }

  @Override
  public void method2() {
    Logger.i(ProxySubject1.class, "我是代理,我会在执行实体方法2之前先做一些预处理的工作");
    subject1.method2();
  }
}

使用代理后我们对 RealSubject1 的操作换成对 ProxySubject1 对象的操作,如下:

ProxySubject1 proxySubject1 = new ProxySubject1(new RealSubject1());
proxySubject1.method1();
proxySubject1.method2();

结果:
[ProxySubject1] : 我是代理,我会在执行实体方法1之前先做一些预处理的工作
[RealSubject1] : 我是RealSubject1的方法1
[ProxySubject1] : 我是代理,我会在执行实体方法2之前先做一些预处理的工作
[RealSubject1] : 我是RealSubject1的方法2
[ProxySubject2] : 我是代理,我会在执行实体方法1之前先做一些预处理的工作
[RealSubject2] : 我是RealSubject2的方法1
[ProxySubject2] : 我是代理,我会在执行实体方法2之前先做一些预处理的工作
[RealSubject2] : 我是RealSubject2的方法2

显然当我们想代理 RealSubject2 按照这种方式我们仍然需要建立一个类去处理,这也是静态代理的局限性。如果写一个代理类就能对上面两个都能代理就好了,动态代理就解决了这个问题。

动态代理

在 java 的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

动态代理的步骤:

  • 写一个 InvocationHandler 的实现类,并实现 invoke 方法,return method.invoke(...);
/**
  * @param proxy 指代我们所代理的那个真实对象
  * @param method 指代的是我们所要调用真实对象的某个方法的Method对象
  * @param args 指代的是调用真实对象某个方法时接受的参数
  */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了一个 handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。

  • 使用 Proxy 类的 newProxyInstance 方法生成一个代理对象。例如: 生成 Subject1 的代理对象,注意第三个参数中要将一个实体对象传入。
/**
  * @param loader 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
  * @param interfaces 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
  * @param h 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上
  */
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

例如:

Proxy.newProxyInstance(
  Subject1.class.getClassLoader(),
  new Class[] {Subject1.class},
  new DynamicProxyHandler(new RealSubject1())
);

Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法。

使用动态代理完成上述静态代理中的功能:

public class DynamicProxyHandler implements InvocationHandler {
  private Object object;

  public DynamicProxyHandler(Object object) {
    this.object = object;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Logger.i(DynamicProxyHandler.class, "我正在动态代理[" + object.getClass().getSimpleName() + "]的[" + method.getName() + "]方法");
    return method.invoke(object, args);
  }

  /**
    * 调用Proxy.newProxyInstance即可生成一个代理对象
    *
    * @param object
    * @return
    */
  public static Object newProxyInstance(Object object) {
    // 传入被代理对象的classloader实现的接口, 还有DynamicProxyHandler的对象即可。
    return Proxy.newProxyInstance(object.getClass().getClassLoader(),
      object.getClass().getInterfaces(),
      new DynamicProxyHandler(object));
  }
}

动态代理调用如下:

Subject1 dynamicProxyHandler1 = (Subject1) DynamicProxyHandler.newProxyInstance(new RealSubject1());
dynamicProxyHandler1.method1();
dynamicProxyHandler1.method2();

初识 Hook 机制

上述我们对一个方法的调用采用了动态代理的办法,如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了,修改参数,替换返回值,我们称之为 Hook。下面我们 Hook 掉 startActivity 这个方法,使得每次调用这个方法之前输出一条日志;当然,这个输入日志有点点弱,只是为了展示原理;只要你想,你想可以替换参数,拦截这个 startActivity 过程,使得调用它导致启动某个别的 Activity,指鹿为马!

首先我们得找到被 Hook 的对象,我称之为 Hook 点;什么样的对象比较好 Hook 呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例。在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的 Hook 点。

对于 startActivity 过程有两种方式:Context.startActivity 和 Activity.startActivity。这里暂不分析其中的区别,以 Activity.startActivity 为例说明整个过程的调用栈。

Activity 中的 startActivity 最终都是由 startActivityForResult 来实现的。

Activity#startActivityForResult:

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
  // 一般的 Activity 其 mParent 为 null,mParent 常用在 ActivityGroup 中,ActivityGroup 已废弃
  if (mParent == null) {
      options = transferSpringboardActivityOptions(options);
      // 这里会启动新的Activity,核心功能都在 mMainThread.getApplicationThread() 中完成
      Instrumentation.ActivityResult ar =
          mInstrumentation.execStartActivity(
              this, mMainThread.getApplicationThread(), mToken, this,
              intent, requestCode, options);
      if (ar != null) {
          mMainThread.sendActivityResult(
              mToken, mEmbeddedID, requestCode, ar.getResultCode(),
              ar.getResultData());
      }
      if (requestCode >= 0) {
          mStartedActivity = true;
      }
      cancelInputsAndStartExitTransition(options);
  } else {
      if (options != null) {
          mParent.startActivityFromChild(this, intent, requestCode, options);
      } else {
          // Note we want to go through this method for compatibility with
          // existing applications that may have overridden it.
          mParent.startActivityFromChild(this, intent, requestCode);
      }
  }
}

可以发现,真正打开 activity 的实现在 Instrumentation 的 execStartActivity 方法中。

Instrumentation#execStartActivity:

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    // 核心功能在这个whoThread中完成,其内部scheduleLaunchActivity方法用于完成activity的打开
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                ActivityResult result = null;
                if (am.ignoreMatchingSpecificIntents()) {
                    result = am.onStartActivity(intent);
                }
                if (result != null) {
                    am.mHits++;
                    return result;
                } else if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        // 这里才是真正打开 Activity 的地方,核心功能在 whoThread 中完成。
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        // 这个方法是专门抛异常的,它会对结果进行检查,如果无法打开activity,
            // 则抛出诸如ActivityNotFoundException类似的各种异常
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

如果我们想深入了解 Activity 启动过程我们需要接着 Android 源码看下去,但是对于本文中我们初步了解 Hook 机制足以。

我们的目的是替换掉系统默认逻辑,对于 Activity#startActivityForResult 的方法里面核心逻辑就是 mInstrumentation 属性的 execStartActivity 方法,而这里的 mInstrumentation 属性在 Activity 类中恰好是一个单例,在 Activity 类的 attach 方法里面被赋值,我们可以在 attach 之后使用反射机制对 mInstrumentation 属性进行重新赋值。attach() 方法调用完成后,就自然而然的调用了 Activity 的 onCreate() 方法了。

我们需要修改 mInstrumentation 这个字段为我们的代理对象,我们使用静态代理实现这个代理对象。这里我们使用 EvilInstrumentation 作为代理对象。

public class EvilInstrumentation extends Instrumentation {
    private Instrumentation instrumentation;

    public EvilInstrumentation(Instrumentation instrumentation) {
        this.instrumentation = instrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        StringBuilder sb = new StringBuilder();
        sb.append("who = [").append(who).append("], ")
        .append("contextThread = [").append(contextThread).append("], ")
        .append("token = [").append(token).append("], ")
        .append("target = [").append(target).append("], ")
        .append("intent = [").append(intent).append("], ")
        .append("requestCode = [").append(requestCode).append("], ")
        .append("options = [").append(options).append("]");;
        Logger.i(EvilInstrumentation.class, "执行了startActivity, 参数如下: " + sb.toString());

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class,
                    IBinder.class,
                    IBinder.class,
                    Activity.class,
                    Intent.class,
                    int.class,
                    Bundle.class);
            return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

采用反射直接修改 Activity 中的 mInstrumentation 属性,从而实现偷梁换柱——用代理对象替换原始对象。

// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);

// 创建代理对象
Instrumentation originalInstrumentation = (Instrumentation) mInstrumentationField.get(activity);
mInstrumentationField.set(activity, new EvilInstrumentation(originalInstrumentation));

这段 Hook 的逻辑放在 Activity 的 onCreate 里面即可生效。

对于 Context 类的 startActivity 方法的 Hook 实现可以参考 weishu 大神的 Android 插件化原理解析——Hook 机制之动态代理,本文也是基于 weishu 大神的文章在学习过程记录的内容。

Activity 启动过程

上述例子中我们只是完成了一个最基础的 Hook 功能,然而大部分插件化框架提供了十分丰富的功能,例如:插件化支持首先要解决的一点就是插件里的 Activity 并未在宿主程序的 AndroidMainfest.xml 注册。常规方法肯定无法直接启动插件的 Activity,这个时候就需要去了解 Activity 的启动流程。

完整的流程如下:

注: 可以在 http://androidxref.com/ 在线查看 Android 源码。

上图列出的是启动一个 Activity 的主要过程,具体步骤如下:

  1. Activity 调用 startActivity,实际会调用 Instrumentation 类的 execStartActivity 方法,Instrumentation 是系统用来监控 Activity 运行的一个类,Activity 的整个生命周期都有它的影子。
  2. 通过跨进程的 Binder 调用,进入到 ActivityManagerService 中,其内部会处理 Activity 栈。之后又通过跨进程调用进入到需要调用的 Activity 所在的进程中。
  3. ApplicationThread 是一个 Binder 对象,其运行在 Binder 线程池中,内部包含一个 H 类,该类继承于类 Handler。ApplicationThread 将启动需要调用的 Activity 的信息通过 H 对象发送给主线程。
  4. 主线程拿到需要调用的 Activity 的信息后,调用 Instrumentation 类的 newActivity 方法,其内通过 ClassLoader 创建 Activity 实例。

下面介绍如何通过 hook 的方式启动插件中的 Activity,需要解决以下两个问题:

  • 插件中的 Activity 没有在 AndroidManifest 中注册,如何绕过检测。
  • 如何构造 Activity 实例,同步生命周期。

我们这里使用最简单的一种实现方式:先在 Manifest 中预埋 StubActivity,启动时 hook 上图第 1 步,将 Intent 替换成 StubActivity。

// StubActivity.java
public class StubActivity extends Activity {
    public static final String TARGET_COMPONENT = "TARGET_COMPONENT";
}

我们上面在 EvilInstrumentation 类里面实现了 execStartActivity 方法,现在我们在这里再加一些额外的逻辑。

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
    Intent intent, int requestCode, Bundle options) {

    StringBuilder sb = new StringBuilder();
    sb.append("who = [").append(who).append("], ")
    .append("contextThread = [").append(contextThread).append("], ")
    .append("token = [").append(token).append("], ")
    .append("target = [").append(target).append("], ")
    .append("intent = [").append(intent).append("], ")
    .append("requestCode = [").append(requestCode).append("], ")
    .append("options = [").append(options).append("]");;
    Logger.i(EvilInstrumentation.class, "执行了startActivity, 参数如下: " + sb.toString());

    // 在此处先将 intent 原本的 Component 保存起来, 然后创建一个新的 intent。
    // 使用 StubActivity 并替换掉原本的 Activity, 以达通过 AMS 验证的目的,然后等 AMS 验证通过后再将其还原。
    Intent replaceIntent = new Intent(target, StubActivity.class);
    replaceIntent.putExtra(StubActivity.TARGET_COMPONENT, intent);
    intent = replaceIntent;

    try {
        Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                "execStartActivity",
                Context.class,
                IBinder.class,
                IBinder.class,
                Activity.class,
                Intent.class,
                int.class,
                Bundle.class);
        return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

通过这种"移花接木"的方式绕过 AMS 验证,但是这里我们并没有完成对我们原本需要真正打开的 Activity 的创建。这里我们需要监听 Activity 的创建过程,然后在适当的适合将原本需要打开的 Activity 还原回来。

ActivityThread 类中有一个重要的消息处理的方法 sendMessage。

2644    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
2645        if (DEBUG_MESSAGES) Slog.v(
2646            TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
2647            + ": " + arg1 + " / " + obj);
2648        Message msg = Message.obtain();
2649        msg.what = what;
2650        msg.obj = obj;
2651        msg.arg1 = arg1;
2652        msg.arg2 = arg2;
2653        if (async) {
2654            msg.setAsynchronous(true);
2655        }
2656        mH.sendMessage(msg);
2657    }

最终都会落实到 mH.sendMessage(msg); 的调用,继续追踪这个 mH 对象,我们会发现是 H 对象的实例化对象。

final H mH = new H();
    private class H extends Handler {
        public void handleMessage(Message msg) {
1585            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1586            switch (msg.what) {
1587                case LAUNCH_ACTIVITY: {
1588                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
1589                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
1590
1591                    r.packageInfo = getPackageInfoNoCheck(
1592                            r.activityInfo.applicationInfo, r.compatInfo);
1593                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
1594                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1595                } break;
        ...
        }
    }

我们知道 Handler 消息机制用于同进程的线程间通信, Handler 是工作线程向 UI 主线程发送消息,工作线程通过 mHandler 向其成员变量 MessageQueue 中添加新 Message,主线程一直处于 loop() 方法内,当收到新的 Message 时按照一定规则分发给相应的 handleMessage() 方法来处理。

类似于对上述 mInstrumentation 实例化对象 hook 一样,这里我们可以对 mH 对象进行 hook。

/**
 * 将替换的activity在此时还原回来
 */
public static void doHandlerHook() {
    try {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
        Object activityThread = currentActivityThread.invoke(null);

        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        Handler mH = (Handler) mHField.get(activityThread);

        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        mCallbackField.set(mH, new ActivityThreadHandlerCallback(mH));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

对于 Handler.Callback 的 hook 实现如下:

public class ActivityThreadHandlerCallback implements Handler.Callback {
    private Handler mBaseHandler;

    public ActivityThreadHandlerCallback(Handler mBaseHandler) {
        this.mBaseHandler = mBaseHandler;
    }

    @Override
    public boolean handleMessage(Message msg) {
        Logger.i(ActivityThreadHandlerCallback.class, "接受到消息了msg:" + msg);

        if (msg.what == 100) {
            try {
                Object obj = msg.obj;
                Field intentField = obj.getClass().getDeclaredField("intent");
                intentField.setAccessible(true);
                Intent intent = (Intent) intentField.get(obj);

                Intent targetIntent = intent.getParcelableExtra(StubActivity.TARGET_COMPONENT);
                intent.setComponent(targetIntent.getComponent());
                Log.e("intentField", targetIntent.toString());
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        mBaseHandler.handleMessage(msg);
        return true;
    }
}

我们设置在 handleMessage 里面还原我们最开始替换的 Activity,至此我们就实现了对于 startActivity 的完整 hook,但是这个过程中仍然存在很多问题,我们需要进一步去深入探索才能去理解和更好实现插件化框架的内容。

学习案例

本文学习案例地址:android-plugin-framework

参考


匠心博客
看似寻常最奇崛,成如容易却艰辛。始终保持一颗匠心去铸造去创造。

看似寻常最奇崛,成如容易却艰辛。

4.6k 声望
1.5k 粉丝
0 条评论
推荐阅读
基于沙盒技术的企业移动应用安全平台设计
移动互联网的飞速发展, 改变了企业传统的业务模式, 提高了工作效率. 但同时也给企业的数据安全带来了巨大的挑战, 我们面对各种攻击的可能性会大 大增加, 面临潜在的风险:

匠心8阅读 5.4k

Java 编译器 javac 及 Lombok 实现原理解析
javac 是 Java 代码的编译器12,初学 Java 的时候就应该接触过。本文整理一些 javac 相关的高级用法。Lombok 库,大家平常一直在使用,但可能并不知道实现原理解析,其实 Lombok 实现上依赖的是 Java 编译器的注...

nullwy10阅读 5.9k

浅谈App的启动优化
温启动:当启动应用时,后台已有该应用的进程,但是Activity可能因为内存不足被回收。这样系统会从已有的进程中来启动这个Activity,这个启动方式叫温启动。

xuexiangjys5阅读 1.6k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.8k

封面图
Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 1.8k评论 2

封面图
Redis 发布订阅模式:原理拆解并实现一个消息队列
“65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?““那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆宣传,暴击单身狗。”像这种 65 哥通过朋...

码哥字节5阅读 1.1k

封面图
NB的Github项目,看到最后一个我惊呆了!
最近看到不少好玩的、实用的 Github 项目,就来给大家推荐一把。中国制霸生成器最近在朋友圈非常火的一个小网站,可以在线标记 居住、短居、游玩、出差、路过 标记后可生成图片进行社区分享,标记过的信息会记录...

艾小仙5阅读 1.5k评论 1

看似寻常最奇崛,成如容易却艰辛。

4.6k 声望
1.5k 粉丝
宣传栏