曾几何时,国内各大公司掀起了一股研究Android动态加载的技术,两年多过去了,动态加载技术俨然成了Android开发中必须掌握的技术。那么动态加载技术是什么呢,这里谈谈我的个人看法,如有雷同,纯属偶然。

什么是动态加载技术

对于动态加载的概念,没有一个权威的定义,参考网上的解释,我们举一个例子,动态加载代码就是通过在运行时加载外部代码(磁盘,网络等)改变程序行为的技术(感觉有点像装饰者模式)。主要目的是为了达到让用户不用重新安装APK就能升级应用的功能。
为了加深大家对这种概念的理解,我们结合pc端来说说手机端的动态加载。

传统的pc端动态加载
熟悉Java的同学应该比较清楚,Java的可执行文件是Jar,运行在虚拟机上JVM上,虚拟机通过ClassLoader加载Jar文件并执行里面的代码。所以Java程序也可以通过动态调用Jar文件达到动态加载的目的。
动态加载技术在PC软件领域广泛使用,比如qq上线的时候忘了某个功能的修复,这个时候就可以用动态加载来修复我们的bug。

Android应用的动态加载技术
Android应用类似于Java程序,只不过虚拟机换成了Dalvik/ART,而Jar换成了Dex。我们知道,在Android的apk文件中往往有一个或者多个Dex文件,系统的类加载器(PathDexClassLoader)加载的就是dex文件,虽然一个apk一旦构建出来,我们是无法更换里面的Dex文件的,但是我们可以在类加载动态加载外部的dex文件来达到动态加载的目的。

JVM 类加载机制

JVM 的类加载机制是双亲委派模型,这里贴上JVM加载的图解。

对于上面这张图,我们有以下几点需要说明。
  • BootStrapClassLoader是顶级的类加载器,它是唯一一个不继承自ClassLoader中的类加载器,它高度集成于 JVM是ExtensionClassLoader的父加载器,它的类加载路径是JDK\jre\lib 和 用户指定的虚拟机参数-Xbootclasspath的值。
  • ExtensionClassLoader 是 BootStrapClassLoader 的子加载器,同时是 SystemClassLoader(有的地方称 AppClassLoader)的父加载器,它的类加载路径是 JDK\jre\lib\ext 和系统属性 java.ext.dirs 的值。
  • SystemClassLoader 是 ExtensionClassLoader 的子加载器,同时是我们的应用程序的类加载器,我们在应用程序中编写的类一般情况下(如果没有到动态加载技术的话)都是通过这个类加载加载的。它的类加载路径是环境变量 CLASSPATH 的值或者用户通过命令行可选项 -cp (-classpath) 指定的值。
  • 类加载器由于父子关系形成树形结构,开发人员可以开发自己的类加载器从而实现动态加载功能,但必须给这个类加载器指定树上的一个节点作为它的父加载器。
  • 因为类加载器是通过包名和类名(或者说类的全限定名),所以由于委派式加载机制的存在,全限定名相同的类不会在有 祖先—子孙 关系的类加载器上分别加载一次,不管这两个类的实现是否一样。
  • 不同的类加载器加载的类一定是不同的类,即使它们的全限定名一样。如果全限定名一样,那么根据上一条,这两个类加载器一定没有 祖先-子孙 的关系。这样来看,可以通过自定义类加载器使得相同全限定名但实现不同的类存在于同一 JVM 中,也就是说,类加载器相当于给类在包名之上又加了个命名空间。
  • 如果两个相同全限定名的类由两个非 祖先-子孙 关系的类加载器加载,这两个类之间通过instanceof 和 equals() 等进行比较时总是返回false

安卓应用和普通的 java 应用不同,它们运行于 Dalvik 虚拟机。JVM 是基于栈的虚拟机,而 Dalvik 是基于寄存器的虚拟机。Android采用 dex 作为储存类字节码信息的文件。当 java 程序编译成 class 后,编译器会使用 dx 工具将所有的class 文件整合到一个 dex 文件,目的是使其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加紧凑。

为了说明Android的类加载机制,我们需要对Android的ClassLoader做一个了解。
安卓中两个重要的类加载器:DexClassLoader 和 PathClassLoader。

那么对于Android来说,我们来看看Android的加载模型。

DexClassLoader & PathClassLoader说明

我们首先看一些这两个类。

package dalvik.system;
import java.io.File;

public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

可以看到,这两个类加载器都是继承自 BaseDexClassLoader,只是分别实现了自己的构造方法。

  • BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

我们发现BaseDexClassLoader作为一个基类,其构造极其简单,它做了两件事:连接了父加载器;构造了一个 DexPathList 实例保存在 pathList 中。

参数意思如下:
  • 第一个参数指的是我们要加载的 dex 文件的路径,它有可能是多个 dex 路径,取决于我们要加载的 dex 文件的个数,多个路径之间用 : 隔开。
  • 第二个参数指的是优化后的 dex 存放目录。实际上,dex 其实还并不能被虚拟机直接加载,它需要系统的优化工具优化后才能真正被利用。优化之后的 dex 文件我们把它叫做 odex (optimized dex,说明这是被优化后的 dex)文件。其实从 class 到 dex 也算是经历了一次优化,这种优化的是机器无关的优化,也就是说不管将来运行在什么机器上,这种优化都是遵循固定模式的,因此这种优化发生在 apk 编译。而从 dex 文件到 odex 文件,是机器相关的优化,它使得 odex 适配于特定的硬件环境,不同机器这一步的优化可能有所不同,所以这一步需要在应用安装等运行时期由机器来完成。需要注意的是,在较早版本的系统中,这个目录可以指定为外部存储中的目录,较新版本的系统为了安全只允许其为应用程序私有存储空间(/data/data/apk-package-name/)下的目录,一般我们可以通过 Context#getDir(String dirName) 得到这个目录。
  • 第三个参数的意义是库文件的的搜索路径,一般来说是 .so 库文件的路径,也可以指明多个路径。
  • 第四个参数就是要传入的父加载器,一般情况我们可以通过 Context#getClassLoader() 得到应用程序的类加载器然后把它传进去。

好了,到这里就很清楚了,Dalvik 虚拟机要加载的 dex 文件的路径(DexPathList),那么Dalvik是如何找到Dex的呢?有人会说反射,对,大方向对了。那么我们看看系统究竟是怎么做的。

  DexPathList
public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }

        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
       }

       if (optimizedDirectory != null) {
           if (!optimizedDirectory.exists())  {
               throw new IllegalArgumentException(
                       "optimizedDirectory doesn't exist: "
                       + optimizedDirectory);
           }

           if (!(optimizedDirectory.canRead()
                           && optimizedDirectory.canWrite())) {
               throw new IllegalArgumentException(
                       "optimizedDirectory not readable/writable: "
                       + optimizedDirectory);
           }
       }

       this.definingContext = definingContext;

       ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
       // save dexPath for BaseDexClassLoader
       this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                              suppressedExceptions, definingContext);

       // Native libraries may exist in both the system and
       // application library paths, and we use this search order:
       //
       //   1. This class loader's library path for application libraries (librarySearchPath):
       //   1.1. Native library directories
       //   1.2. Path to libraries in apk-files
       //   2. The VM's library path from the system property for system libraries
       //      also known as java.library.path
       //
       // This order was reversed prior to Gingerbread; see http://b/2933456.
       this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
       this.systemNativeLibraryDirectories =
               splitPaths(System.getProperty("java.library.path"), true);
       List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
       allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

       this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
                                                         suppressedExceptions,
                                                         definingContext);

       if (suppressedExceptions.size() > 0) {
           this.dexElementsSuppressedExceptions =
               suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
       } else {
           dexElementsSuppressedExceptions = null;
       }
}

这里我们主要看如下几行代码:

his.definingContext = definingContext;

       ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
       // save dexPath for BaseDexClassLoader
       this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                              suppressedExceptions, definingContext);

       // Native libraries may exist in both the system and
       // application library paths, and we use this search order:
       //
       //   1. This class loader's library path for application libraries (librarySearchPath):
       //   1.1. Native library directories
       //   1.2. Path to libraries in apk-files
       //   2. The VM's library path from the system property for system libraries
       //      also known as java.library.path
       //
       // This order was reversed prior to Gingerbread; see http://b/2933456.
       this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
       this.systemNativeLibraryDirectories =
               splitPaths(System.getProperty("java.library.path"), true);
       List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
       allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

       this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
                                                         suppressedExceptions,
                                                         definingContext);

这段代码主要是给 dexElements和nativeLibraryPathElements赋值。我们知道Android在通过默认的虚拟机dex后,会继续优化为odex 文件。

dexElements 是通过 makeDexElements() 方法得到的。makeDexElements的方法里面我们主要关注前面两个参数,我们来看一下splitDexPath(dexPath)。
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
       List<File> result = new ArrayList<>();

       if (searchPath != null) {
           for (String path : searchPath.split(File.pathSeparator)) {
               if (directoriesOnly) {
                   try {
                       StructStat sb = Libcore.os.stat(path);
                       if (!S_ISDIR(sb.st_mode)) {
                           continue;
                       }
                   } catch (ErrnoException ignored) {
                       continue;
                   }
               }
               result.add(new File(path));
           }
       }

       return result;
}

这个方法很简单就是用,分隔的路径分割后保存为 File 类型的列表返回。现在看看 makeDexElements() 这个方法:

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                                            List<IOException> suppressedExceptions,
                                            ClassLoader loader) {
     return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
}
private static Element[] makeElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions,
                                          boolean ignoreDexFiles,
                                         ClassLoader loader) {
       Element[] elements = new Element[files.size()];
       int elementsPos = 0;
       /*
        * Open all files and load the (direct or contained) dex files
        * up front.
        */
       for (File file : files) {
           File zip = null;
           File dir = new File("");
           DexFile dex = null;
           String path = file.getPath();
           String name = file.getName();

           if (path.contains(zipSeparator)) {
               String split[] = path.split(zipSeparator, 2);
               zip = new File(split[0]);
               dir = new File(split[1]);
           } else if (file.isDirectory()) {
               // We support directories for looking up resources and native libraries.
               // Looking up resources in directories is useful for running libcore tests.
               elements[elementsPos++] = new Element(file, true, null, null);
           } else if (file.isFile()) {
               if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                   // Raw dex file (not inside a zip/jar).
                   try {
                       dex = loadDexFile(file, optimizedDirectory, loader, elements);
                   } catch (IOException suppressed) {
                       System.logE("Unable to load dex file: " + file, suppressed);
                       suppressedExceptions.add(suppressed);
                   }
               } else {
                   zip = file;

                   if (!ignoreDexFiles) {
                       try {
                           dex = loadDexFile(file, optimizedDirectory, loader, elements);
                       } catch (IOException suppressed) {
                           /*
                            * IOException might get thrown "legitimately" by the DexFile constructor if
                            * the zip file turns out to be resource-only (that is, no classes.dex file
                            * in it).
                            * Let dex == null and hang on to the exception to add to the tea-leaves for
                            * when findClass returns null.
                            */
                           suppressedExceptions.add(suppressed);
                       }
                   }
               }
           } else {
               System.logW("ClassLoader referenced unknown path: " + file);
           }

           if ((zip != null) || (dex != null)) {
               elements[elementsPos++] = new Element(dir, false, zip, dex);
           }
        }
       if (elementsPos != elements.length) {
           elements = Arrays.copyOf(elements, elementsPos);
       }
       return elements;
}

通过代码我们可以大致了解到,这个方法就是将之前的File对象通过重新组合成一个新的Elements对象,然后我们Loader读取的就是Element对象。看一下 loadDexFile() 怎样加载 DexFile 的

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                      Element[] elements) throws IOException {
       if (optimizedDirectory == null) {
           return new DexFile(file, loader, elements);
       } else {
           String optimizedPath = optimizedPathFor(file, optimizedDirectory);
           return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
       }
}

先说明下无论是 DexFile(File file, Classloader loader, Elements[] elements) 还是 DexFile.loadDex() 最终都会调用 DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements) 这个构造方法。所以这个方法的逻辑就是:如果 optimizedDirectory 为 null,那么就直接利用 file 的路径构造一个 DexFile;否则就根据要加载的 dex(或者包含了 dex 的 zip) 的文件名和优化后的 dex 存放的目录组合成优化后的 dex(也就是 odex)文件的输出路径,然后利用原始路径和优化后的输出路径构造出一个DexFile.

分析完这两字段,现在我们回过头来看看 DexPathList 这个对象,这个对象持有 dexElements 和 nativeLibraryPathElements 这两个属性,也就是说它保存了 dex 和 本地方法库。

为了加深大家对DexPathList的理解,我们来看看官方的说明。
A pair of lists of entries, associated with a {@code ClassLoader}. One of the lists is a dex/resource path — typically referred to as a “class path” — list, and the other names directories containing native code libraries. Class path entries may be any of: a {@code .jar} or {@code .zip} file containing an optional top-level {@code classes.dex} file as well as arbitrary resources, or a plain {@code .dex} file (with no possibility of associated resources).</br>This class also contains methods to use these lists to look up classes and resources.

大概的意思就是 DexPathList 的作用和 JVM 中的 classpath 的作用类似,JVM 根据 classpath 来查找类,而 Dalvik 利用 DexPathList 来查找并加载类。DexPathList 包含的路径可以是 .dex 文件的路径,也可以是包含了 dex 的 .jar 和 .zip 文件的路径。

BaseClassLoader 加载器的类加载过程

我们知道,一个类加载器的入口方法是 loadClass()。这是Java语音所共有的。类加载器通过findClass()找到所需要加载的类。

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

BaseDexClassLoader 也继承自 ClassLoader,因此我们就从 findClass() 方法来分析下 BaseClassLoader 加载类的过程。

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
       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;
}

这个方法极其简单,主要风格findclass找到类 Class c = pathList.findClass(name, suppressedException)这里BaseClassLoader 把查找类的任务委托给了 pathList。那么我们来看一下Android的DexPathList的findClass又做了什么事情。

public Class findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {
           DexFile dex = element.dexFile;

           if (dex != null) {
               Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
               if (clazz != null) {
                   return clazz;
               }
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
}

它遍历了 dexElements 中的所有 DexFile,通过 DexFile 的loadClassBinaryName() 方法加载目标类。dexElements 又把查找类的任务委托给了DexFile

private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
           DexPathList.Element[] elements) throws IOException {
       if (outputName != null) {
           try {
               String parent = new File(outputName).getParent();
               if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                   throw new IllegalArgumentException("Optimized data directory " + parent
                           + " is not owned by the current user. Shared storage cannot protect"
                           + " your application from code injection attacks.");
               }
           } catch (ErrnoException ignored) {
               // assume we'll fail with a more contextual error later
           }
       }

       mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
       mFileName = sourceName;
       //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}

到这里我们就已经很明白了,openDexFile调用openDexFileNative()方法,(

mCookie = openDexFile(sourceName, outputName, flags, loader, elements);

它做的事就是把对应的 dex 文件加载到内存中,然后返回给 java 层一个类似句柄一样的东西 Object:mCookie。

在构造方法中 DexFile 就完成了 dex 文件的加载过程。现在我们回到 DexFile 对象的loadClassBinaryName()

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
       return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                    DexFile dexFile, List<Throwable> suppressed) {
       Class result = null;
       try {
           result = defineClassNative(name, loader, cookie, dexFile);
       } catch (NoClassDefFoundError e) {
           if (suppressed != null) {
               suppressed.add(e);
           }
       } catch (ClassNotFoundException e) {
           if (suppressed != null) {
               suppressed.add(e);
           }
       }
       return result;
}

看到这里我们明白了,class 对象在 java 层加载过程的尽头就是这个 defineClass() 方法,这个方法调用本地法 defineClassNative() 从 dex 中查找目标类,如果找到了,就把这个代表这个类的 Class 对象返回。到此,Android的加载过程我们终于看完了。

到这里我们回头看看Android的两个类加载器:DexClassLoader() 和 PathClassLoader()。
DexClassLoader 用来加载 .dex 文件以及包含 dex 文件的 .jar、.zip 和未安装的 .apk 文件,因此需要指定优化后的 dex 文件的输出路径;
PathClassLoader 一般用来加载已经安装到设备上的.apk,因为应用在安装的时候已经对 apk 文件中的 dex 进行了优化,并且会输出到 /data/dalvik-cache 目录下(android M 在这目录下找不到,应该是改成了 /data/app/com.example.app-x/oat 目录下),所以它不需要指定优化后 dex 的输出路径。
常用的插件化和动态加载都是基于DexClassLoader来实现的,如果有需要的请点链接   点击打开链接

Android动态加载入坑指南的更多相关文章

  1. Android动态加载技术初探

    一.前言: 现在,已经有实力强大的公司用这个技术开发应用了,比如淘宝,大众点评,百度地图等,之所以采用这个技术,实际上,就是方便更新功能,当然,前提是新旧功能的接口一致,不然会报Not Found等错 ...

  2. Android动态加载jar/dex

    前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优 ...

  3. [转载] Android动态加载Dex机制解析

    本文转载自: http://blog.csdn.net/wy353208214/article/details/50859422 1.什么是类加载器? 类加载器(class loader)是 Java ...

  4. Android 动态加载 (二) 态加载机制 案例二

    探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法 重要说明 在实践的过程中大家都会发现资源引用的问题,这里重点声明两点: 1. 资源文件是不能直接inflate的,如果简单的话直接在程序 ...

  5. Android 动态加载 (一) 态加载机制 案例一

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

  6. Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类

    前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流! 关键字:Android动态加载 声明 欢迎转载, ...

  7. Android动态加载代码技术

    Android动态加载代码技术 在开发Android App的过程当中,可能希望实现插件式软件架构,将一部分代码以另外一个APK的形式单独发布,而在主程序中加载并执行这个APK中的代码. 实现这个任务 ...

  8. 【Android】Android动态加载Jar、APK的实现

    本文介绍Android中动态加载Jar.APK的实现.而主要用到的就是DexClassLoader这个类.大家都知道Android和普通的Java虚拟机有差别,它只能加载经过处理的dex文件.而加载这 ...

  9. 深入浅出Android动态加载jar包技术

    在实际项目中,由于某些业务频繁变更而导致频繁升级客户端的弊病会造成较差的用户体验,而这也恰是Web App的优势,于是便衍生了一种思路,将核心的易于变更的业务封装在jar包里然后通过网络下载下来,再由 ...

随机推荐

  1. CodeForces - 724G:Xor-matic Number of the Graph

    两点之间的任意路径都可表示为  随便某一条路径xor任何多个环, 然后可以用线性基来做,这样不会重复的, 另外必须一位一位的处理,xor是不满足结合律的 #include<cstdio> ...

  2. bzoj 1407: [Noi2002]Savage

    Description 解题报告: 因为给定答案范围,暴力枚举时间,然后再两两枚举野人,判断是否有可能在某一年相遇,我们设这一年为\(x\),那么显然相交的条件是: \(x*(p[i]-p[j])+y ...

  3. hdu 4777 树状数组+合数分解

    Rabbit Kingdom Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) T ...

  4. 【集训第二天·翻水的老师】--ac自动机+splay树

    今天是第二天集训.(其实已经是第三天了,只是昨天并没有机会来写总结,现在补上) 上午大家心情都很愉快,因为老师讲了splay树和ac自动机. 但到了下午,我们的教练竟然跑出去耍了(excuse me? ...

  5. bzoj省选十连测推广赛

    A.普通计算姬 题意:给丁一棵树,每个点有一个权值,用sum(x)表示以x为根的子树的权值和,要求支持两种操作: 1 u v  :修改点u的权值为v. 2 l  r   :  求∑sum[i] l&l ...

  6. Jenkins简明入门(三) -- Blue Ocean,让一切变得简单

    我们在上一节Jenkins简明入门(二) 中见识到了Jenkins能做些什么:利用Jenkins完成python程序的build.test.deployment. 同时,也有一种简单的方法,不需要写J ...

  7. python2.7入门---操作mysql数据库增删改查

    Python 标准数据库接口为 Python DB-API,Python DB-API为开发人员提供了数据库应用编程接口.Python 数据库接口支持非常多的数据库,你可以选择适合你项目的数据库: G ...

  8. Servlet生命周期与工作原理(转载)

    Servlet生命周期分为三个阶段: 1,初始化阶段  调用init()方法 2,响应客户请求阶段 调用service()方法 3,终止阶段 调用destroy()方法 Servlet初始化阶段: 在 ...

  9. 开源Spring解决方案--lm.solution

    Github 项目地址: https://github.com/liumeng0403/lm.solution 一.说明 1.本项目未按java项目传统命名方式命名项目名,包名 如:org.xxxx. ...

  10. OpenCv error :unresolved external symbol(链接库没有加上)

    Error 如下:Linking...: error LNK2001: unresolved external symbol _cvDestroyWindow: error LNK2001: unre ...