Android热修复及插件化原理示例详解

目录
  • 1.前言
  • 2.类加载机制
  • 3.Android类加载
  • 4.Tinker原理
    • 代码实现
  • 5.插件化
    • 5.1 Activity启动流程简单介绍
    • 5.2 插件化原理
      • 5.2.1 绕开验证
      • 5.2.2还原插件Activity
    • 5.3 加载插件资源
      • 5.3.1 Resources&AssetManager
      • 5.3.2 id冲突

1.前言

热修复一直是这几年来很热门的话题,主流方案大致有两种,一种是微信Tinker的dex文件替换,另一种是阿里的Native层的方法替换。这里重点介绍Tinker的大致原理。

2.类加载机制

介绍Tinker原理之前,我们先来回顾一下类加载机制。

我们编译好的class文件,需要先加载到虚拟机然后才会执行,这个过程是通过ClassLoader来完成的。

双亲委派模型:

  • 1.加载某个类的时候,这个类加载器不会自己立刻去加载,它会委托给父类去加载
  • 2.如果这个父类还存在父类加载器,则进一步委托,直到最顶层的类加载器
  • 3.如果父类加载器可以完成加载任务,就成功返回,否则就再委派给子类加载器
  • 4.如果都未加载成功就抛出ClassNotFoundException

作用:

1.避免类的重复加载。

比如有两个类加载器,他们都要加载同一个类,这时候如果不是委托而是自己加载自己的,则会将类重复加载到方法区。

2.避免核心类被修改。

比如我们在自定义一个 java.lang.String 类,执行的时候会报错,因为 String 是 java.lang 包下的类,应该由启动类加载器加载。

JVM并不会一开始就加载所有的类,它是当你使用到的时候才会去通知类加载器去加载。

3.Android类加载

当我们new一个类时,首先是Android的虚拟机(Dalvik/ART虚拟机)通过ClassLoader去加载dex文件到内存。

Android中的ClassLoader主要是PathClassLoader和DexClassLoader,这两者都继承自BaseDexClassLoader。它们都可以理解成应用类加载器。

PathClassLoader和DexClassLoader的区别:

  • PathClassLoader只能指定加载apk包路径,不能指定dex文件解压路径。该路径是写死的在/data/dalvik-cache/路径下。所以只能用于加载已安装的apk。
  • DexClassLoader可以指定apk包路径和dex文件解压路径(加载jar、apk、dex文件)

当ClassLoader加载类时,会调用它的findclass方法去查找该类。

下方是BaseDexClassLoader的findClass方法实现:

public class BaseDexClassLoader extends ClassLoader {
...
    @UnsupportedAppUsage
    private final DexPathList pathList;
    ...
     @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 首先检查该类是否存在shared libraries中.
        if (sharedLibraryLoaders != null) {
            for (ClassLoader loader : sharedLibraryLoaders) {
                try {
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        //再调用pathList.findClass去查找该类,结果为null则抛出错误。
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

接下来我们再来看看DexPathList的findClass实现:

 public DexPathList(ClassLoader definingContext, String librarySearchPath) {
...
    /**
     * List of dex/resource (class path) elements.
     * 存放dex文件的一个数组
     */
    @UnsupportedAppUsage
    private Element[] dexElements;
...
    public Class&lt;?&gt; findClass(String name, List&lt;Throwable&gt; suppressed) {
     	 //遍历Element数组,去查寻对应的类,找到后就立刻返回了
        for (Element element : dexElements) {
            Class&lt;?&gt; clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
...
}

4.Tinker原理

  • 1.使用DexClassLoader加载补丁包的dex文件
  • 2.通过反射获取DexClassLoader类的pathList,再次通过反射获得dexElements数组。
  • 3.获取加载应用类的PathClassLoader,同样通过反射获取它的dexElements数组。
  • 4.合并两个dexElements数组,且将补丁包的dex文件放在前面。
    根据类加载机制,一个类只会被加载一次,DexPathList.findClass方法中是顺序遍历数组,所以将补丁的dex文件放在前面,这样bug修复类会被优先加载,而原来的bug类不会被加载,达到了替换bug类的功能(补丁包中的修复类名、包名要和bug类相同)
  • 5.再次通过反射将合并后的dexElements数组赋值给PathClassLoader.dexElements属性。
    加载类时,Dalvik/ART虚拟机会通过PathClassLoader去查找已安装的apk文件中的类。

Ok,这样就替换成功了,重启App,再调用原来的bug类,将会优先使用补丁包中的修复类。

为什么要重启:因为双亲委派模型,一个类只会被ClassLoader加载一次,且加载过后的类不能卸载。

代码实现

接下来我们动手撸一个乞丐版的Tinker。
首先我们写一个bug类。

package com.baima.plugin;
class BugClass {
    public String getTitle(){
        return "这是个Bug";
    }
}

接着我们新建一个module来生成补丁包apk。

创建bug修复类,注意包名类名要一样。

package com.baima.plugin;
class BugClass {
    public String getTitle(){
        return "修复成功";
    }
}

生成补丁apk,让用户下载这个补丁包。接下来就是加载这个apk文件并替换了。

    public void loadDexAndInject(Context appContext, String dexPath, String dexOptPath) {
        try {
            // 加载应用程序dex的Loader
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            //dexPath 补丁dex文件所在的路径
            //dexOptPath 补丁dex文件被写入后存放的路径
            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOptPath, null, pathLoader);
            //利用反射获取DexClassLoader和PathClassLoader的pathList属性
            Object dexPathList = getPathList(dexClassLoader);
            Object pathPathList = getPathList(pathLoader);
            //同样用反射获取DexClassLoader和PathClassLoader的dexElements属性
            Object leftDexElements = getDexElements(dexPathList);
            Object rightDexElements = getDexElements(pathPathList);
            //合并两个数组,且补丁包的dex文件在数组的前面
            Object dexElements = combineArray(leftDexElements, rightDexElements);
            //反射将合并后的数组赋值给PathClassLoader的pathList.dexElements
            Object pathList = getPathList(pathLoader);
            Class<?> pathClazz = pathList.getClass();
            Field declaredField = pathClazz.getDeclaredField("dexElements");
            declaredField.set看,ccessible(true);
            declaredField.set(pathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static Object getPathList(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
        Field field = cl.getDeclaredField("pathList");
        field.setAccessible(true);
        return field.get(classLoader);
    }
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        Class<?> cl = pathList.getClass();
        Field field = cl.getDeclaredField("dexElements");
        field.setAccessible(true);
        return field.get(pathList);
    }
    private static Object combineArray(Object arrayLeft, Object arrayRight) {
        Class<?> clazz = arrayLeft.getClass().getComponentType();
        int i = Array.getLength(arrayLeft);
        int j = Array.getLength(arrayRight);
        int k = i + j;
        Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组
        System.arraycopy(arrayLeft, 0, result, 0, i);
        System.arraycopy(arrayRight, 0, result, i, j);
        return result;
    }

ok,乞丐版Tinker完成了,使用时先在Splash界面检查是否有插件补丁,有的话执行替换,这时你再使用bug类会发现它已经被替换成补丁中的修复类了。

5.插件化

插件化开发模式,打包时是一个宿主apk+多个插件apk。

组件化开发模式,打包时是一个apk,里面分多个module。

优点:

  • 安装的主apk包会小好多
  • 给开发者提供了业务功能扩展,并且不需要用户进行更新
  • 在非主apk包中的功能出现BUG时,可以及时修复
  • 用户不需要的功能,完全就不会出现在系统里面,减轻设备的负担

需要掌握的知识:

  • 1.类加载机制
  • 2.四大组件启动流程
  • 3.AIDL、Binder机制
  • 4.Hook、反射、代理

5.1 Activity启动流程简单介绍

上图是普通的Activity启动流程,和根Activity启动流程的区别是不用创建应用程序进程(Application Thread)。

启动过程:

  • 应用程序进程中的Activity向AMS请求创建普通Activity
  • AMS会对这个Activty的生命周期管和栈进行管理,校验Activity等等
  • 如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity

他们之间的跨进程通信是通过Binder实现的。

5.2 插件化原理

通过上面介绍的热修复,我们有办法去加载插件apk里面的类,但是还没有办法去启动插件中的Activity,因为如果要启动一个Activity,那么这个Activity必须在AndroidManifest.xml中注册。

这里介绍插件化的一种主流实现方式--Hook技术。

  • 1.宿主App预留占坑Activity
  • 2.使用classLoader加载dex文件到内存
  • 3.先使用占坑Activity绕过AMS验证,接着用插件Activity替换占坑的Activity。

步骤1、2这里就不在赘述了,2就是上面讲到的热修复技术。

5.2.1 绕开验证

AMS是在SystemServer进程中,我们无法直接进行修改,只能在应用程序进程中做文章。

介绍一个类--IActivityManager,IActivityManager它通过AIDL(内部使用的是Binder机制)和SystemServer进程的AMS通讯。所以IActivityManager很适合作为一个hook点。

Activity启动时会调用IActivityManager.startActivity方法向AMS发出启动请求,该方法参数包含一个Intent对象,它是原本要启动的Activity的Intent。

我们可以动态代理IActivityManager的startActivity方法,将该Intent换为占坑Activity的Intent,并将原来的Intent作为参数传递过去,以此达到欺骗AMS绕开验证。

public class IActivityManagerProxy implements InvocationHandler {
    private Object mActivityManager;
    private static final String TAG = "IActivityManagerProxy";
    public IActivityManagerProxy(Object activityManager) {
        this.mActivityManager = activityManager;
    }
    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())) {
            Intent intent = null;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            intent = (Intent) args[index];
            Intent subIntent = new Intent();
            String packageName = "com.example.pluginactivity";
            subIntent.setClassName(packageName,packageName+".StubActivity");
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

接下来就通过反射的方式,将ActivityManager中的IActivityManager替换成我们的代理对象。

  public void hookAMS() {
        try {
            Object defaultSingleton = null;
            if (Build.VERSION.SDK_INT >= 26) {
                Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
                defaultSingleton = FieldUtil.getObjectField(activityManagerClazz, null, "IActivityManagerSingleton");
            } else {
                Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
                defaultSingleton = FieldUtil.getObjectField(activityManagerNativeClazz, null, "gDefault");
            }
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
            Object iActivityManager = mInstanceField.get(defaultSingleton);
            Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));
            mInstanceField.set(defaultSingleton, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Note: 这里获取IActivityManager实例会因为Android版本不同而不同,具体获取方法就需要去看源码了解了。这里的代码Android 8.0是可以运行的。

5.2.2还原插件Activity

ActivityThread启动Activity的过程如下所示:

ActivityThread会通过H在主线程中去启动Activity,H类是ActivityThread的内部类并继承自Handler。

private class H extends Handler {
public static final int LAUNCH_ACTIVITY         = 100;
public static final int PAUSE_ACTIVITY          = 101;
...
   public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, "&gt;&gt;&gt; handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                ...
              }
...
}

H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。那么在哪进行替换呢?接着来看Handler的dispatchMessage方法:

public void dispatchMessage(Message msg) {
       if (msg.callback != null) {
           handleCallback(msg);
       } else {
           if (mCallback != null) {
               if (mCallback.handleMessage(msg)) {
                   return;
               }
           }
           handleMessage(msg);
       }
   }

Handler的dispatchMessage用于处理消息,可以看到如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。因此,mCallback可以作为Hook点,我们可以用自定义的Callback来替换mCallback,自定义的Callback如下所示。

public class HCallback implements Handler.Callback{
    public static final int LAUNCH_ACTIVITY = 100;
    Handler mHandler;
    public HCallback(Handler handler) {
        mHandler = handler;
    }
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到消息中的Intent(启动占坑Activity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //得到此前保存起来的Intent(启动插件Activity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //将占坑Activity的Intent替换为插件Activity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

最后一步就是用反射将我们自定义的callBack设置给ActivityThread.sCurrentActivityThread.mH.mCallback

    public void hookHandler() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = FieldUtil.getObjectField(activityThreadClass, null, "sCurrentActivityThread");
            Field mHField = FieldUtil.getField(activityThreadClass, "mH");
            Handler mH = (Handler) mHField.get(currentActivityThread);
            FieldUtil.setObjectField(Handler.class, mH, "mCallback", new HCallback(mH));
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

其实要想启动一个Activity到这步还没有完,一个完整的Activity应该还需要布局文件,而我们的宿主APP并不会包含插件的资源。

5.3 加载插件资源

5.3.1 Resources&AssetManager

android中的资源大致分为两类:一类是res目录下存在的可编译的资源文件,比如anim,string之类的,第二类是assets目录下存放的原始资源文件。因为Apk编译的时候不会编译这些文件,所以不能通过id来访问,当然也不能通过绝对路径来访问。于是Android系统让我们通过Resources的getAssets方法来获取AssetManager,利用AssetManager来访问这些文件。

其实Resource的getString, getText等各种方法都是通过调用AssetManager的私有方法来完成的。 过程就是Resource通过resource.arsc(AAPT工具打包过程中生成的文件)把ID转换成资源文件的名称,然后交由AssetManager来加载文件。

AssetManager里有个很重要的方法addAssetPath(String path)方法,App启动的时候会把当前apk的路径传进去,然后AssetManager就能访问这个路径下的所有资源也就是宿主apk的资源了。我们可以通过hook这个方法将插件的path传进去,得到的AssetManager就能同时访问宿主和插件的所有资源了。

 public void hookAssets(Activity activity,String dexPath){
        try {
            AssetManager assetManager = activity.getResources().getAssets();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,dexPath);
            Resources mResources = new Resources(assetManager, activity.getResources().getDisplayMetrics(), activity.getResources().getConfiguration());
            //接下来我们要将宿主原有Resources替换成我们上面生成的Resources。
            FieldUtil.setObjectField(ContextWrapper.class,activity.getResources(),"mResources",mResources);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

5.3.2 id冲突

新的问题又出现了,宿主apk和插件apk是两个不同的apk,他们在编译时都会产生自己的resources.arsc。即他们是两个独立的编译过程。那么它们的resources.arsc中的资源id必定是有相同的情况。这样我们上面生成的新Resources中就出现了资源id重复的情况,这样在运行的时候使用资源id来获取资源就会报错。

怎么解决资源Id冲突的问题?这里介绍一下VirtualApk采用的方案。

修改aapt的产物。即编译后期重新整理插件Apk的资源,编排ID,更新R文件

VirtualApkhook了ProcessAndroidResourcestask。这个task是用来编译Android资源的。VirtualApk拿到这个task的输出结果,做了以下处理:

  • 1.根据编译产生的R.txt文件收集插件中所有的资源
  • 2.根据编译产生的R.txt文件收集宿主apk中的所有资源
  • 3.过滤插件资源:过滤掉在宿主中已经存在的资源
  • 4.重新设置插件资源的资源ID
  • 5.删除掉插件资源目录下前面已经被过滤掉的资源
  • 6.重新编排插件resources.arsc文件中插件资源ID为新设置的资源ID
  • 7.重新产生R.java文件

大致原理是这样的,但如何保证新的Id不会重复了,这里在介绍一下资源Id的组成。

packageId: 前两位是packageId,相当于一个命名空间,主要用来区分不同的包空间(不是不同的module)。目前来看,在编译app的时候,至少会遇到两个包空间:android系统资源包和咱们自己的App资源包。大家可以观察R.java文件,可以看到部分是以0x01开头的,部分是以0x7f开头的。以0x01开头的就是系统已经内置的资源id,以0x7f开头的是咱们自己添加的app资源id。

typeId:typeId是指资源的类型id,我们知道android资源有animator、anim、color、drawable、layout,string等等,typeId就是拿来区分不同的资源类型。

entryId:entryId是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。

所以为了避免冲突,插件的资源id通常会采用0x02 - 0x7e之间的数值。

以上就是Android热修复及插件化原理详解的详细内容,更多关于Android热修复插件化的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入理解Android热修复技术原理之代码热修复技术

    一.底层热替换原理 1.1.Andfix 回顾 我们先来看一下,为何唯独 Andfix 能够做到即时生效呢? 原因是这样的,在 app运行到一半的时候,所有需要发生变更的分类已经被加载过了,在Android 上是无法对一个分类进行卸载的.而腾讯系的方案,都是让 Classloader去加载新的类.如果不重启,原来的类还在虚拟机中,就无法加载新类.因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve 为新的类.从而达到热修复的目的. An

  • Android热修复Tinker接入及源码解读

    一.概述 热修复这项技术,基本上已经成为项目比较重要的模块了.主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了. 现在热修复的技术基本上有阿里的AndFix.QZone的方案.美团提出的思想方案以及腾讯的Tinker等. 其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的:QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);

  • 深入理解Android热修复技术原理之资源热修复技术

    一.普遍的实现方式 目前市面上的很多资源热修复方案基本上都是参考了 Instant Run的实现. 简要说来,Instant Run中的资源热修复分为两步: 1.构造一个新的 AssetManager,并通过反射调用 addAssetPath,把这个完 整的新资源包加入到AssetManager中.这样就得到了一个含有所有新资源的 AssetManager. 2.找到所有之前引用到原有 AssetManager的地方,通过反射,把引用处替换 为 AssetManager. 一个 Android

  • 深入理解Android热修复技术原理之so库热修复技术

    目录 一.SO库加载原理 二.SO库热部署实时生效可行性分析 2.1.动态注册 native 方法实时生效 2.2.静态注册 native 方法实时生效 2.3.SO实时生效方案总结 三.SO库冷部署重启生效实现方案 3.1.接口调用替换方案 3.2.反射注入方案 四.如何正确复制补丁 SO库 五.本章小结 一.SO库加载原理 Java Api 提供以下两个接口加载一个 so 库 System. loadLibrary (String libName):传进去的参数:so库名称, 表示的so 库

  • Android接入阿里云热修复介绍

    1.AndroidManinifest.xml中加入权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permissio

  • Android热修复及插件化原理示例详解

    目录 1.前言 2.类加载机制 3.Android类加载 4.Tinker原理 代码实现 5.插件化 5.1 Activity启动流程简单介绍 5.2 插件化原理 5.2.1 绕开验证 5.2.2还原插件Activity 5.3 加载插件资源 5.3.1 Resources&AssetManager 5.3.2 id冲突 1.前言 热修复一直是这几年来很热门的话题,主流方案大致有两种,一种是微信Tinker的dex文件替换,另一种是阿里的Native层的方法替换.这里重点介绍Tinker的大致原

  • Android 插件化处理方案详解

    目录 插件化启动Activity的过程 资源冲突的解决方案 resources.arsc资源描述符详解 解决冲突的方案 插件化启动Activity的过程 在宿主里面的AndroidManifest.xml里面注册一个空的activity 从开始执行execStartActivity到最终将Activity对象new出来这个过程,系统层会去校验需要启动的activity的合法性[就是是否有在某个应用的AndroidManifest.xml里面注册]以及按启动要求创建activity对象.清晰了这点

  • Array.reduce使用原理示例详解

    目录 正文 重新了解 Array.reduce reduce 的运用场景 用于计算数据 将多维数组转为一维数组 将函数作为参数 其他场景 最后 正文 我们经常会用到 Array 对象的 reduce 方法,把它用于做一些计算.或者数据组合,发现自己用了那么多年 reduce ,竟然还不是很了解它,最近才发现如果不传递初始值,它也可以正常进行,数组也可以是一个函数数组,来增强我们的代码. 本篇文章将带你重来了解 Array.reduce 和运用场景. 重新了解 Array.reduce 我们来看一

  • jQuery.Validate表单验证插件的使用示例详解

    jQuery Validate 插件为表单提供了强大的验证功能,让客户端表单验证变得更简单,同时提供了大量的定制选项,满足应用程序各种需求. 请在这里查看示例 validate示例 示例包含 验证错误时,显示红色错误提示 自定义验证规则 引入中文错误提示 重置表单需要执行2句话 源码示例 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <

  • Sentinel熔断规则原理示例详解分析

    目录 概述 熔断(降级)策略 慢调用比例 概念 测试 异常比例 概念 测试 异常数 概念 测试 概述 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一. 由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积. Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时.异常比例升高.异常数堆积) 对这个资源的调用进行限制,让请求快速失败从而避免影响到其它的资源而导致级联错误. 当资源被降级后,在接下来的降级时间窗口之内

  • C++递归与分治算法原理示例详解

    目录 1. 汉诺塔问题 2. 全排列问题 4. 归并排序 5. 快速排序 6. 棋盘覆盖问题 1. 汉诺塔问题 递归算法,分为 3 步:将 n 个 a 上的盘子借助 c 移动到 b ① 将 n-1 个 a 上的盘子借助 b 移动到 c ② 将 a 上的盘子移动到 b ③ 将 c 上的 n-1 个盘子借助 a 移动到 b 所有盘子都移动到 b 上了 void hanoi(int n,char a,char b,char c)//将n个碟子从a借助c 移到b { if(n==0) return; e

  • Android开发两个activity之间传值示例详解

    目录 使用Inten的putExtra传递 使用Intention的Bundle传递 使用Activity销毁时传递数据 SharedPreferences传递数据 使用序列化对象Seriazable 使用静态变量传递数据 handler 使用Inten的putExtra传递 第一个Activity中 //创建意图对象 Intent intent = new Intent(this,MainActivity2.class); //设置传递键值对 intent.putExtra("name&quo

  • Kotlin协程Dispatchers原理示例详解

    目录 前置知识 demo startCoroutineCancellable intercepted()函数 DefaultScheduler中找dispatch函数 Runnable传入 Worker线程执行逻辑 小结 前置知识 Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中.所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行. 之

  • babel插件去除console示例详解

    目录 起因 介绍 窥探 初见AST Program ExpressionStatement CallExpression MemberExpression Identifier StringLiteral 公共属性 如何写一个babel插件? 构造visitor方法 去除所有console 增加env api 增加exclude api 增加commentWords api 细节完善 对于后缀注释 对于前缀注释 发布到线上 安装 使用 起因 已经颓废了很久 因为实在不知道写啥了 突然我某个同事对

  • java编程FinalReference与Finalizer原理示例详解

    之前写了一篇java编程Reference核心原理示例源码分析的文章,但由于篇幅和时间的原因没有给出FinalReference和Finalizer的分析.同时也没有说明为什么建议不要重写Object#finalize方法(实际上JDK9已经将Object#finalize方法标记为Deprecated).将文章转发到perfma社区后,社区便有同学提出一个有意思的问题?"Object#finalize如果在执行的时候当前对象又被重新赋值,那下次GC就不会再执行finalize方法了,这是为什么

随机推荐