diff --git a/.gitignore b/.gitignore index 71986661..c647db94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /.project /.DS_Store -/.DS_Store +/.idea diff --git "a/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" new file mode 100644 index 00000000..1abe582f --- /dev/null +++ "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" @@ -0,0 +1,731 @@ + +热修复实现(一) +=== + + +现在的热修复方案已经有很多了,例如`alibaba`的[dexposed](https://github.com/alibaba/dexposed)、[AndFix](https://github.com/alibaba/AndFix)以及`jasonross`的[Nuwa](https://github.com/jasonross/Nuwa)等等。原来没有仔细去分析过也没想写这篇文章,但是之前[InstantRun详解][1]这篇文章中介绍了`Android Studio Instant Run`的 +实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。 + + +方案有很多种,我就只说明下我想到的方式,也就是`Instant Run`的方式: +分拆到不同的`dex`中,然后通过`classloader`来进行加载。但是在之前[`InstantRun`详解](https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md)中只说到会通过内部的`server`去判断该类是否有更新,如果有的话就去从新的`dex`中加载该类,否则就从旧的`dex`中加载,但这是如何实现的呢? 怎么去从不同的`dex`中选择最新的那个来进行加载。 + +讲到这里需要先介绍一下`ClassLoader`: + +< `A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.` + + +在一般情况下,应用程序不需要创建`ClassLoader`对象,而是使用当前环境已经存在的`ClassLoader`。因为`Java`的`Runtime`环境在初始化时,其内部会创建一个`ClassLoader`对象用于加载`Runtime`所需的各种`Java`类。 +每个`ClassLoader`必须有一个父类,在装载`Class`文件时,子`ClassLoader`会先请求父`ClassLoader`加载该`Class`文件,只有当其父`ClassLoader`找不到该`Class`文件时,子`ClassLoader`才会继续装载该类,这是一种安全机制。 + +对于`Android`的应用程序,本质上虽然也是用`Java`开发,并且使用标准的`Java`编译器编译出`Class`文件,但最终的`APK`文件中包含的却是`dex`类型的文件。`dex`文件是将所需的所有`Class`文件重新打包,打包的规则不是简单的压缩,而是完全对`Class`文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是`dex`文件。由于`dex`文件是一种经过优化的`Class`文件,因此要加载这样特殊的`Class`文件就需要特殊的类装载器,这就是`DexClassLoader`,`Android SDK`中提供的`DexClassLoader`类就是出于这个目的。 + + +总体来说,`Android` 默认主要有三个`ClassLoader`: + +- `BootClassLoader`: 系统启动时创建 + `Provides an explicit representation of the boot class loader. It sits at the + head of the class loader chain and delegates requests to the VM's internal + class loading mechanism.` +- `PathClassLoader`: 可以加载`/data/app`目录下的`apk`,这也意味着,它只能加载已经安装的`apk`; +- `DexClassLoader`: 可以加载文件系统上的`jar`、`dex`、`apk`;可以从`SD`卡中加载未安装的`apk` + + +通过上面的分析知道,如果用多个`dex`的话肯定会用到`DexClassLoader`类,我们首先来看一下它的源码(这里 +插一嘴,源码可以去[googlesource](https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system)中找): +```java +/** + * A class loader that loads classes from {@code .jar} and {@code .apk} files + * containing a {@code classes.dex} entry. This can be used to execute code not + * installed as part of an application. + * + *
This class loader requires an application-private, writable directory to + * cache optimized classes. Use {@code Context.getDir(String, int)} to create + * such a directory:
{@code
+ * File dexOutputDir = context.getDir("dex", 0);
+ * }
+ *
+ * Do not cache optimized classes on external storage. + * External storage does not provide access controls necessary to protect your + * application from code injection attacks. + */ +public class DexClassLoader extends BaseDexClassLoader { + /** + * Creates a {@code DexClassLoader} that finds interpreted and native + * code. Interpreted classes are found in a set of DEX files contained + * in Jar or APK files. + * + *
The path lists are separated using the character specified by the + * {@code path.separator} system property, which defaults to {@code :}. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; must not be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public DexClassLoader(String dexPath, String optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(dexPath, new File(optimizedDirectory), libraryPath, parent); + } +} +``` +注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, +因为加载都是从`loadClass()`方法中,所以我们去`ClassLoader`类中看一下`loadClass()`方法: +```java +/** + * Loads the class with the specified name. Invoking this method is + * equivalent to calling {@code loadClass(className, false)}. + *
+ * Note: In the Android reference implementation, the + * second parameter of {@link #loadClass(String, boolean)} is ignored + * anyway. + *
+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @throws ClassNotFoundException + * if the class can not be found. + */ + public Class> loadClass(String className) throws ClassNotFoundException { + return loadClass(className, false); + } + + /** + * Loads the class with the specified name, optionally linking it after + * loading. The following steps are performed: + *+ * Note: In the Android reference implementation, the + * {@code resolve} parameter is ignored; classes are never linked. + *
+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @param resolve + * Indicates if the class should be resolved after loading. This + * parameter is ignored on the Android reference implementation; + * classes are not resolved. + * @throws ClassNotFoundException + * if the class can not be found. + */ + protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException { + Class> clazz = findLoadedClass(className); + + if (clazz == null) { + ClassNotFoundException suppressed = null; + try { + // 先检查父ClassLoader是否已经加载过该类 + clazz = parent.loadClass(className, false); + } catch (ClassNotFoundException e) { + suppressed = e; + } + + if (clazz == null) { + try { + // 调用DexClassLoader.findClass()方法。 + clazz = findClass(className); + } catch (ClassNotFoundException e) { + e.addSuppressed(suppressed); + throw e; + } + } + } + + return clazz; + } +``` +上面会调用`DexClassLoader.findClass()`方法,但是`DexClassLoader`没有实现该方法,所以去它的父类`BaseDexClassLoader`中看,接着看一下`BaseDexClassLoader`的源码: +```java +/** + * Base class for common functionality between various dex-based + * {@link ClassLoader} implementations. + */ +public class BaseDexClassLoader extends ClassLoader { + /** originally specified path (just used for {@code toString()}) */ + private final String originalPath; + /** structured lists of path elements */ + private final DexPathList pathList; + /** + * Constructs an instance. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; may be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public BaseDexClassLoader(String dexPath, File optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(parent); + this.originalPath = dexPath; + this.pathList = + new DexPathList(this, dexPath, libraryPath, optimizedDirectory); + } + @Override + protected Class> findClass(String name) throws ClassNotFoundException { + // 从DexPathList中找 + Class clazz = pathList.findClass(name); + if (clazz == null) { + throw new ClassNotFoundException(name); + } + return clazz; + } + @Override + protected URL findResource(String name) { + return pathList.findResource(name); + } + @Override + protected EnumerationThis class also contains methods to use these lists to look up + * classes and resources.
+ */ +/*package*/ final class DexPathList { + private static final String DEX_SUFFIX = ".dex"; + private static final String JAR_SUFFIX = ".jar"; + private static final String ZIP_SUFFIX = ".zip"; + private static final String APK_SUFFIX = ".apk"; + /** class definition context */ + private final ClassLoader definingContext; + /** list of dex/resource (class path) elements */ + // 把dex封装成一个数组,每个Element代表一个dex + private final Element[] dexElements; + /** list of native library directory elements */ + private final File[] nativeLibraryDirectories; + + // ..... + + /** + * Finds the named class in one of the dex files pointed at by + * this instance. This will find the one in the earliest listed + * path element. If the class is found but has not yet been + * defined, then this method will define it in the defining + * context that this instance was constructed with. + * + * @return the named class or {@code null} if the class is not + * found in any of the dex files + */ + public Class findClass(String name) { + for (Element element : dexElements) { + DexFile dex = element.dexFile; + // 遍历数组,拿到第一个就返回 + if (dex != null) { + Class clazz = dex.loadClassBinaryName(name, definingContext); + if (clazz != null) { + return clazz; + } + } + } + return null; + } +} +``` +从上面的源码中分析,我知道系统会把所有相关的`dex`维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的`dex`对应的`Element`放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的`dex`中的类。 +所以方案出来了,只要把有问题的类修复后,放到一个单独的`dex`,然后把该`Dex`转换成对应的`Element`后再将该`Element`插入到`dexElements`数组的第一个位置就可以了。那该如何去将其插入到`dexElements`数组的第一个位置呢?-- 暴力反射。 + + + +到这里我感觉初步的思路已经有了: + +- 将补丁作为`dex`发布。 +- 通过反射修改该`dex`所对应的`Element`在数组中的位置。 + +但是我也想到肯定还会有类似下面的问题: + +- 资源文件的处理 +- 四大组件的处理 +- 清单文件的处理 + + +虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。 + +好了,`demo`走起来。 + + +怎么生成`dex`文件呢? 这要讲过两部分: + +- `.class`-> `.jar` : `jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class` +- `.jar`-> `.dex`: `dx --dex --output=target.jar test.jar` `target.jar`就是包含`.dex`的`jar`包 + + +生成好`dex`后我们为了模拟先将其放到`asset`目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该`dex`转换成 + + +方案中采用的是`MultiDex`,对其进行一部分改造,具体代码: + +- 添加`dex`文件,并执行`install` + +```java +/** +* 添加apk包外的dex文件 +* 自动执行install +* @param dexFile +*/ +public static void addDexFileAutoInstall(Context context, List
+
+##### 申请需要的权限
如果应用没有所需的权限时,应用必须调用`ActivityCompat.requestPermissions (Activity activity,
String[] permissions,
@@ -145,9 +146,9 @@ if (ContextCompat.checkSelfPermission(thisActivity,
}
```
-> ***注意:***当调用`requestPermissions()`方法时,系统会显示一个标准的对话框。应用不能指定或者改变该对话框。如果你想提供一些信息或者说明给用户,你需要在调用`requestPermissions()`之前处理。
+> 注意:当调用`requestPermissions()`方法时,系统会显示一个标准的对话框。应用不能指定或者改变该对话框。如果你想提供一些信息或者说明给用户,你需要在调用`requestPermissions()`之前处理。
-#####处理请求权限的的结果
+##### 处理请求权限的的结果
如果应用申请权限,系统会显示一个对话框。当用户相应后,系统会调用应用中的`onRequestPermissionsResult (int requestCode,
String[] permissions,
@@ -183,7 +184,7 @@ public void onRequestPermissionsResult(int requestCode,
系统提示的对话框会描述应用所需的`permission groud`。它不会列出特定的权限。例如,如果你申请了`READ_CONTACTS`权限,系统的对话框只会说你的应用需要获取设备的联系人信息。用户只需要授权每个`permission group`一次。如果你应用需要申请其他任何一个在该`permission group`中的权限时,系统会自动授权。在申请这些授权时,系统会像用户明确通过系统对话框统一授权时一样去调用`onRequestPermissionsResult()`方法并且传递`PERMISSION_GRANTED`参数。
-> ***注意:***虽然用户已经授权了同一`permission group`中其他的任何权限,但是应用仍然需要明确申请每个需要的权限。例外,`permission group`中的权限在以后可能会发生变化。
+> 注意:虽然用户已经授权了同一`permission group`中其他的任何权限,但是应用仍然需要明确申请每个需要的权限。例外,`permission group`中的权限在以后可能会发生变化。
例如,假设在应用的`manifest`文件中同时声明了`READ_CONTACTS`和`WRITE_CONTACTS`权限。如果你申请`READ_CONTACTS`权限而且用户同意了该权限,如果你想继续申请`WRITE_CONTACTS`权限,系统不会与用户有任何交互就会直接进行授权。
diff --git "a/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md"
new file mode 100644
index 00000000..7efca459
--- /dev/null
+++ "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md"
@@ -0,0 +1,161 @@
+AndroidRuntime_ART与Dalvik
+===
+在说Android Runtime之前,我们需要了解什么是运行时环境,还需要了解一些基本知识,即JVM和Dalvik VM的功能。
+
+## Runtime
+
+用最简单的术语来说,它是操作系统使用的系统,它负责将您用Java之类的高级语言编写的代码转换为CPU/处理器能理解机器代码。
+
+运行时包含在程序运行时执行的软件指令,即使它们实际上并不是该软件代码的一部分也是如此。
+CPU或更笼统的术语我们的计算机仅理解机器语言(二进制代码),因此要使其在CPU上运行,必须将代码转换为机器代码,这由翻译器完成。
+因此,以下是按顺序生成翻译器的过程:
+- Assemblers
+ 它可以直接将汇编代码转换为机器代码,因此速度非常快。
+- Compilers
+ 它将代码转换为汇编代码,然后使用汇编程序将代码转换为二进制。使用此编译速度很慢,但是执行速度很快。但是编译器的最大问题是所生成的机器代码取决于平台。换句话说,在一台计算机上运行的代码可能不会在另一台计算机上运行。
+- Interpreters
+ 它在执行代码时翻译代码。由于翻译是在运行时进行的,因此执行速度很慢。
+### JVM
+为了维持代码的平台独立性,JAVA开发了JVM,即Java虚拟机。它针对每个平台开发了JVM,这意味着JVM依赖于该平台。Java编译器将.java文件转换为.class文件,这称为字节码。该字节码被提供给JVM,该JVM将其转换为机器码。
+
+### Android Runtime
+
+当我们构建应用程序并生成APK时,该APK的一部分是.dex文件。这些文件包含我们应用程序的源代码,包括我们在为软件解释器设计的低级代码(字节码)中使用的所有库。
+
+当用户运行我们的应用程序时,写入的.dex文件中的字节码将由Android Runtime转换为机器码—一组指令,机器可以直接理解并由CPU处理。
+
+
+
+Android Runtime同样也回管理内存及垃圾回收。
+
+
+
+
+
+
+
+可以使用各种策略将字节码编译为机器代码,所有这些策略都有其取舍。为了了解Android Runtime的工作原理,需要首先了解Dalvik。
+
+
+
+## Android Runtime的发展
+
+### Dalvik(<= Android K)
+
+`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行,
+`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
+很长时间以来,`Dalvik`虚拟机一直被用户指责为拖慢安卓系统运行速度不如`IOS`的根源。
+`2014`年`6`月`25`日,`Android L`正式亮相于召开的谷歌`I/O`大会,`Android L`改动幅度较大,谷歌将直接删除`Dalvik`,代替它的是传闻已久的`ART`。
+
+
+
+早期,Android智能手机并不像现在那么强大。大多数手机的RAM很少,有些甚至只有200MB。
+难怪第一个被称为Dalvik的Android Runtime的实现正是为了优化此参数:RAM的使用。
+
+因此,它没有在运行它之前将整个应用程序编译为机器代码,而是使用了称为Just In Time编译(简称JIT)的策略。
+
+在这种策略下,编译器可以充当解释器。它在应用程序执行期间(在运行时)编译一小段代码。
+
+而且由于Dalvik仅编译所需的代码并在运行时执行它,因此可以节省大量RAM。
+
+使用Dalvik JIT编译器,每次运行该应用程序时,它会将Dalvik字节码的一部分动态转换为机器代码。随着执行的进行,更多的字节码将被编译和缓存。由于JIT仅编译部分代码,因此它具有较小的内存占用空间,并且在设备上使用的物理空间更少。
+
+但是此策略有一个严重的缺点-因为所有这些都在运行时发生,因此显然会对运行时性能产生负面影响。
+
+最终,引入了一些优化以使Dalvik更具性能。一些经常使用的已编译代码段已被缓存,不再重新编译。但这是非常有限的,因为在最初的日子里内存非常稀缺。
+
+
+
+几年来运行良好,但与此同时,手机的性能越来越高,RAM也越来越多。而且由于应用程序也越来越大,因此JIT性能影响变得越来越成问题。
+
+这就是为什么在Android L中引入了新的Android Runtime:ART。
+
+
+
+### ART(Android L)
+
+`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。
+`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。
+
+`ART`使用`AOT(Ahead Of Time)`(静态编译)而`Dalvik`使用`JIT(Just In Time)`(动态编译)
+`JIT`方式会在程序执行时将`Dex bytecode`(`java`字节码)转换为处理器可以理解的本地代码,这种方式会将编译时间计入程序的执行时间,程序执行会显得慢一些。`AOT`方式会在程序执行之前(一般是安装时)就编译好本地代码,因此程序执行时少了编译的过程会显得快一些,但占用更多存储空间,安装时也会更慢。但没针对ART优化的程序反而会运行得更慢,随着`Android L`的普及这个问题迟早会解决。`ART`拥有改进的`GC`(垃圾回收)机制:`GC`时更少的暂停时间、`GC`时并行处理、某些时候`Collector`所需时间更短、减少内存不足时触发GC的次数、减少后台内存占用。
+在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。
+
+
+
+`AOT`的编译器分两种模式:
+
+- 在开发机上编译预装应用;
+- 在设备上编译新安装的应用,在应用安装时将`dex`字节码翻译成本地机器码。
+
+
+
+这种方法极大地提高了运行时性能,因为运行本机机器代码甚至比即时编译快20倍。
+
+ART优点:
+
+- 系统性能的显著提升。
+- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。
+- 更长的电池续航能力。
+- 支持更低的硬件。
+
+ART缺点:
+
+- 更大的存储空间占用,可能会增加10%-20%。字节码预先编译成机器码并存储到本地,机器码需要的存储空间更大。
+- 更长的应用安装时间,因为下载APK后,整个应用程序都需要转换为机器代码,而且由于所有应用程序都需要重新优化,因此执行系统更新还需要更长的时间。
+- Android L中的ART使用的内存比Dalvik多得多。
+
+
+
+对于应用程序中经常运行的部分来说,对其进行预编译显然是有回报的,但现实是,用户很少打开应用程序的大多数部分,而对整个应用程序进行预编译几乎也没有回报。
+
+这就是为什么在Android N中,Just In Time编译与称为配置文件引导编译(profile-guided complication)一起被引入到Android Runtime的原因。
+
+### Profile-guided compilation(Android N)
+
+配置文件引导编译是一种策略,可以在运行Android应用程序时不断提高其性能。默认情况下,应用程序使用即时编译策略进行编译,但是当ART检测到某些热点功能时,这意味着它们经常运行,ART可以预编译并缓存这些方法以获得最佳性能。
+
+
+
+该策略可为应用程序的关键部分提供最佳性能,同时减少RAM使用量。由于事实证明,对于大多数应用程序来说,通常仅使用10%到20%的代码。
+
+更改ART之后,不再影响应用程序安装和系统更新的速度。应用的关键部分的预编译仅在设备空闲和充电时进行,以最大程度地减少对设备电池的影响。
+
+
+
+这种方法的唯一缺点是,为了获取配置文件数据并预编译常用的方法和类,用户必须实际使用应用程序。这意味着该应用程序的一些首次使用可能会有点慢,因为在这种情况下,将仅使用即时编译。
+
+这就是为什么要改善在Android P中的初始用户体验的原因,Google在云中引入了个人资料。
+
+### Profiles in the cloud(Android P)
+
+云中的配置文件背后的主要思想是,大多数人以非常相似的方式使用该应用程序。因此,为了在安装后立即提高性能,我们可以从已经使用过此应用程序的人那里收集配置文件数据。此汇总的配置文件数据用于为应用程序创建一个称为通用核心配置文件的文件。
+
+
+
+因此,当新用户安装该应用程序时,该文件将与该应用程序一起下载。 ART使用它来预编译大多数用户经常运行的类和方法。这样,新用户在下载应用程序后即可获得更好的性能。
+
+这并不意味着不再使用旧策略。用户运行应用程序后,ART将收集用户特定的配置文件数据并重新编译设备闲置时该特定用户经常使用的代码。
+
+而最好的部分是,我们开发应用程序的开发人员无需执行任何操作即可启用此功能。这一切都发生在Android Runtime中。
+
+
+
+
+
+- 最初Android使用Dalvik,它使用即时编译(JIT)来优化RAM(那时内存是非常紧缺的)的使用。
+- 为了提高Android L中的性能,引入了使用Ahead of time编译的ART。这样可以实现更好的运行时性能,但会导致更长的安装时间和更多的RAM使用率。
+- 因此,在Android N中,JIT被引入到ART中,并且配置文件引导的编译允许为经常运行的部分代码提供更好的性能。
+- 为了让用户在Android P中安装应用后立即获得最佳性能,Google在云端引入了配置文件,它通过添加随APK下载的通用核心配置文件来补充以前的优化,并允许ART预编译部分代码最常由以前的应用程序用户运行。
+
+
+
+
+
+[Android Runtime-How Dalvik and ART works?](https://www.youtube.com/watch?v=0J1bm585UCc&t=27s)
+
+
+---
+
+- 邮箱 :charon.chui@gmail.com
+- Good Luck!
diff --git "a/Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" "b/AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md"
similarity index 95%
rename from "Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md"
rename to "AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md"
index 95bc3f31..b6b73b52 100644
--- "a/Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md"
+++ "b/AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md"
@@ -129,8 +129,11 @@ void Java_com_charon_uninstallfeedback_MainActivity_initUninstallFeedback(
- 编译so文件。`Windows`下要用`cygwin`来操作。
上面的介绍是在`Eclipse`中进行的,用`ndk-build`命令来编译`so`。具体请看之前写的`JNI基础`这篇文章。
-有关如何在`Android Stuido`中进行`ndk`开发请看另一篇文章。
+有关如何在[Android Stuido中进行ndk开发请看][1]。
+
+[1]: https://github.com/CharonChui/AndroidNote/blob/master/AndroidStudioCourse/AndroidStudio%E4%B8%AD%E8%BF%9B%E8%A1%8Cndk%E5%BC%80%E5%8F%91.md "Android Stuido中进行ndk开发"
+
---
- 邮箱 :charon.chui@gmail.com
diff --git "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md"
similarity index 65%
rename from "Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md"
rename to "AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md"
index 4b2e9aa4..5008840f 100644
--- "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md"
+++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md"
@@ -1,11 +1,11 @@
Android启动模式详解
===
-- standard
- 默认模式。在该模式下,`Activity`可以拥有多个实例,并且这些实例既可以位于同一个task,也可以位于不同的task。每次都会新创建。
-- singleTop
+- `standard`
+ 默认模式。在该模式下,`Activity`可以拥有多个实例,并且这些实例既可以位于同一个`task`,也可以位于不同的`task`。每次都会新创建。
+- `singleTop`
该模式下,在同一个`task`中,如果存在该`Activity`的实例,并且该`Activity`实例位于栈顶则不会创建该`Activity`的示例,而仅仅只是调用`Activity`的`onNewIntent()`。否则的话,则新建该`Activity`的实例,并将其置于栈顶。
-- singleTask
+- `singleTask`
顾名思义,只容许有一个包含该`Activity`实例的`task`存在!
在`android`浏览器`browser`中,`BrowserActivity`的`launcherMode="singleTask"`,因为`browser`不断地启动自己,所以要求这个栈中保持只能有一个自己的实例,`browser`上网的时候,
遇到播放视频的链接,就会通过隐式`intent`方式跳转找`Gallery3D`中的`MovieView`这个类来播放视频,这时候如果你点击`home`键,再点击`browser`,你会发现`MovieView`这个类已经销毁不存在了,
@@ -16,30 +16,50 @@ Android启动模式详解
以`singleTask`方式启动的`Activity`,全局只有唯一个实例存在,因此,当我们第一次启动这个`Activity`时,系统便会创建一个新的任务栈,并且初始化一个`Activity`实例,放在新任务栈的底部,如果下次再启动这个`Activity`时,
系统发现已经存在这样的`Activity`实例,就会调用这个`Activity`实例的`onNewIntent`方法,从而把它激活起来。从这句话就可以推断出,以`singleTask`方式启动的`Activity`总是属于一个任务栈的根`Activity`。
下面我们看一下示例图:
- 
+ 
坑爹啊!有木有!前面刚说`singleTask`会在新的任务中运行,并且位于任务堆栈的底部,这里在`Task B`中,一个赤裸裸的带着`singleTask`标签的箭头无情地指向`Task B`堆栈顶端的`Activity Y`,什么鬼?
这其实是和`taskAffinity`有关,在将要启动时,系统会根据要启动的`Activity`的`taskAffinity`属性值在系统中查找这样的一个`Task`:`Task`的`affinity`属性值与即将要启动的`Activity`的`taskAffinity`属性值一致。如果存在,
就返回这个`Task`堆栈顶端的`Activity`回去,不重新创建任务栈了,再去启动另外一个`singletask`的`activity`时就会在跟它有相同`taskAffinity`的任务中启动,并且位于这个任务的堆栈顶端,于是,前面那个图中,
就会出现一个带着`singleTask`标签的箭头指向一个任务堆栈顶端的`Activity Y`了。在上面的`AndroidManifest.xml`文件中,没有配置`MainActivity`和`SubActivity`的`taskAffinity`属性,
于是它们的`taskAffinity`属性值就默认为父标签`application`的`taskAffinity`属性值,这里,标签`application`的`taskAffinity`也没有配置,于是它们就默认为包名。
-总的来说:`singleTask`的结论与`android:taskAffinity`相关:
+总的来说:`singleTask`的结论与`android:taskAffinity`相关:
- 设置了`singleTask`启动模式的`Activity`,它在启动的时候,会先在系统中查找属性值`affinity`等于它的属性值`taskAffinity`的任务栈的存在;如果存在这样的任务栈,它就会在这个任务栈中启动,否则就会在新任务栈中启动。
因此,如果我们想要设置了`singleTask`启动模式的`Activity`在新的任务栈中启动,就要为它设置一个独立的`taskAffinity`属性值。以`A`启动`B`来说当`A`和`B`的`taskAffinity`相同时:第一次创建`B`的实例时,并不会启动新的`task`,
而是直接将`B`添加到`A`所在的`task`;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。
- 如果设置了`singleTask`启动模式的`Activity`不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的`Activity`实例,如果存在,就会把位于这个`Activity`实例上面的`Activity`全部结束掉,
即最终这个Activity实例会位于任务的堆栈顶端中。以`A`启动`B`来说,当`A`和`B`的`taskAffinity`不同时:第一次创建`B`的实例时,会启动新的`task`,然后将`B`添加到新建的`task`中;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。
-- singleInstance
+- `singleInstance`
+
顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例!
-它与`singleTask`有相同之处,也有不同之处。
-相同之处:任意时刻,最多只允许存在一个实例。
-不同之处:
- - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。
- - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。
- - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时,
- 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。
+它与`singleTask`有相同之处,也有不同之处。
+相同之处: 任意时刻,最多只允许存在一个实例。
+不同之处:
+
+- `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。
+- `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。
+- 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时,不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。
+
+
+假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?
+
+使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。
+而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。
+
+假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。
+现在FirstActivity 启动SecondActivity,SecondActivity再启动ThirdActivity。
+
+然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。
+
+
+
+
+
+
+
+
---
- 邮箱 :charon.chui@gmail.com
-- Good Luck!
\ No newline at end of file
+- Good Luck!
diff --git "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md"
similarity index 88%
rename from "Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md"
rename to "AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md"
index 7d1ac6df..102de73a 100644
--- "a/Android\345\212\240\345\274\272/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md"
+++ "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md"
@@ -13,7 +13,7 @@ Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:1234567890"))
startActivity(intent);
```
-**错!**,你通过这段代码需要该权限的原因是因为你可以在任何时间在不需要用户操作的情况下打电话。也就是说如果我的应用申请了这个权限,我可以在你不知情的情况下每天凌晨三点去拨打骚扰电话。
+错!,你通过这段代码需要该权限的原因是因为你可以在任何时间在不需要用户操作的情况下打电话。也就是说如果我的应用申请了这个权限,我可以在你不知情的情况下每天凌晨三点去拨打骚扰电话。
正确的方式是使用`ACTION_VIEW`或者`ACTION_DIAL`:
```java
@@ -22,7 +22,7 @@ intent.setData(Uri.parse("tel:1234567890"))
startActivity(intent);
```
-**这个问题的完美解决方案就是不需要申请权限了。**原因就是你不是直接拨号,而是用指定的号码调起拨号器,仍然需要用户点击”拨号”来开始打电话。老实的说,这样让人感觉更好。
+这个问题的完美解决方案就是不需要申请权限了。原因就是你不是直接拨号,而是用指定的号码调起拨号器,仍然需要用户点击”拨号”来开始打电话。老实的说,这样让人感觉更好。
简单的说就是如果我想要的操作不是让用户在应用内点击某个按钮就直接开始拨打电话,而是让用户点击在应用内点击某个按钮是我们去调起拨号程序,并且显示指定号码,让用户在拨号器中点击拨号后再开始拨打电话。这样的话我们就完全不用申请拨号权限了。
diff --git "a/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md"
new file mode 100644
index 00000000..4653b0a9
--- /dev/null
+++ "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md"
@@ -0,0 +1,600 @@
+Android开发中的MVP模式详解
+===
+
+[MVC、MVP、MVVM介绍][1]
+
+
+
+在`Android`开发中,如果不注重架构的话,`Activity`类就会变得愈发庞大。这是因为在`Android`开发中`View`和其他的线程可以共存于`Activity`内。那最大的问题是什么呢? 其实就是`Activity`中同时存在业务逻辑和`UI`逻辑。这导致增加了单元测试和维护的成本。
+
+
+
+这就是为什么要清晰架构的原因之一。不仅是因为`Activity`类变得臃肿,也是其他的一些问题,例如`Activity`和`Fragment`相结合时的生命周期、数据绑定等等。
+
+### MVP简介
+
+`MVP(Model,View,Presenter)`
+
+- `View`:负责处理用户时间和视图展现。在`Android`中就可能是`Activity`或者`Fragment`。
+- `Model`: 负责数据访问。数据可以是从接口或者本地数据库中获取。
+- `Presenter`: 负责连接`View`和`Model`。
+
+用一句话来说:`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。
+
+清晰的架构:
+
+
+
+
+举个栗子:
+在`Android Studio`中新建一个`Activity`,系统提供了`LoginActivity`,直接用它是极好的。
+
+
+
+不得不说,`Material Design`的效果真是美美哒!
+
+好,那我们就用用户登录页来按照`MVP`的模式实现一下:
+
+- M: 很显然Model应该是`User`类。
+- V: `View`就是`LoginActivity`。
+- P: P那我们一会就创建一个`LoginPresenter`类。
+
+齐了,那接下来就详细分析下他们这三部分:
+
+- `User`: 应该有`email`, `password`, `boolean login(email, password)`。
+- `LoginActivity`:点击登录应该要出`loading`页。登录成功后要进入下一个页面。如果登录失败应该弹`toast`提示。那就需要`void showLoading()`,`void hideLoading()`,`void showErrorTip()`,`void doLoginSuccess()`这四个方法。
+- `LoginPresenter`:这是`Model`和`View`的桥梁。他需要做的处理业务逻辑,直接与`Model`打交道,然后将`UI`的逻辑交给`LoginActivity`处理。
+那怎么做呢? 按照我上面总结的那一句话,、`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。很显然我们需要提供一个接口。那就新建一个`ILoginView`的接口。这里面有哪些方法呢? 当然是上面我们在分析`LoginActiity`时提出的那四个方法。这样`LoginActivity`直接实现`ILoginView`接口就好。
+
+
+开始做:
+
+- 先把`Model`做好吧,创建`User`类。
+
+ ```java
+ public class User {
+ private String email;
+ private String password;
+ public User(String email, String password) {
+ this.email = email;
+ this.password = password;
+ }
+
+ public boolean login() {
+ // do login request..
+ return true;
+ }
+ }
+ ```
+
+- 创建`ILoginView`接口,定义登录所需要的`ui`逻辑。
+
+ ```java
+ public interface ILoginView {
+ void showLoading();
+ void hideLoading();
+ void showErrorTip();
+ void doLoginSuccess();
+ }
+ ```
+
+- 创建`LoginPresenter`类,使用`ILoginView`接口,那该类主要有什么功能呢? 它主要是处理业务逻辑的,
+ 对于登录的话,当然是用户在`UI`页面输入邮箱和密码,然后`Presenter`去开线程、请求接口。然后得到登录结果再去让`UI`显示对应的视图。那自然就是有一个`void login(String email, String passowrd)`的方法了
+
+ ```java
+ public class LoginPresenter {
+ private ILoginView mLoginView;
+
+ public LoginPresenter(ILoginView loginView) {
+ mLoginView = loginView;
+ }
+
+ public void login(String email, String password) {
+ if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password)) {
+ //
+ mLoginView.showErrorTip();
+ return;
+ }
+ mLoginView.showLoading();
+ User user = new User(email, password);
+
+ // do network request....
+ // ....
+ onSuccess() {
+ boolean login = user.login();
+ if (login) {
+ mLoginView.doLoginSuccess();
+ } else {
+ mLoginView.showErrorTip();
+ }
+ mLoginView.hideLoading();
+ }
+
+ onFailde() {
+ mLoginView.showErrorTip();
+ mLoginView.hideLoading();
+ }
+ }
+ }
+ ```
+- 创建`LoginActivity`,实现`ILoginView`的接口,然后内部调用`LoginPresenter`来处理业务逻辑。
+
+ ```java
+ public class LoginActivity extends AppCompatActivity implements ILoginView {
+ private LoginPresenter mLoginPresenter;
+
+ private AutoCompleteTextView mEmailView;
+ private EditText mPasswordView;
+ private View mProgressView;
+ private View mLoginButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_login);
+ mEmailView = (AutoCompleteTextView) findViewById(R.id.email);
+ mPasswordView = (EditText) findViewById(R.id.password);
+ mLoginButton = findViewById(R.id.email_sign_in_button);
+ mProgressView = findViewById(R.id.login_progress);
+
+ mLoginPresenter = new LoginPresenter(this);
+
+ mLoginButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLoginPresenter.login(mEmailView.getText().toString().trim(), mPasswordView.getText().toString().trim());
+ }
+ });
+ }
+
+ @Override
+ public void showLoading() {
+ mProgressView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void hideLoading() {
+ mProgressView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void showErrorTip() {
+ Toast.makeText(this, "login faled", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void doLoginSuccess() {
+ Toast.makeText(this, "login success", Toast.LENGTH_SHORT).show();
+ }
+ }
+ ```
+
+---
+
+
+上面只是抛砖引玉。`MVP`的优点十分明显,就是代码解耦、可以让逻辑清晰,但是同样它也会有缺点,它的缺点就是项目的复杂程度会增加,项目中会多出很多类。
+之前很多人都在讨论该如何去正确的设计使用`MVP`来避免它的缺点,众说纷纭,很多人讨论的你死我活。直到`Google`发布了`MVP架构蓝图`,大家才意识到这才是规范。
+
+项目地址:[android-architecture](https://github.com/googlesamples/android-architecture)
+`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。
+
+在它的官方介绍中是这样说的:
+
+> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult.
+
+> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools.
+
+> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple.
+
+
+
+已完成的示例:
+
+- todo-mvp/ - Basic Model-View-Presenter architecture.
+- todo-mvp-loaders/ - Based on todo-mvp, fetches data using Loaders.
+- todo-mvp-databinding/ - Based on todo-mvp, uses the Data Binding Library.
+- todo-mvp-clean/ - Based on todo-mvp, uses concepts from Clean Architecture.
+- todo-mvp-dagger/ - Based on todo-mvp, uses Dagger2 for Dependency Injection
+- todo-mvp-contentproviders/ - Based on todo-mvp-loaders, fetches data using Loaders and uses Content Providers
+- todo-mvp-rxjava/ - Based on todo-mvp, uses RxJava for concurrency and data layer abstraction.
+
+
+我们接下来就用`todo-mvp`来进行分析,这个应用非常简单,主要有以下几个功能:
+
+- 列表页:展示所有的`todo`项
+- 添加页:添加`todo`项
+- 详情页:查看`todo`项的详情
+- 统计页:查看当前所有已完成`todo`及未完成项的统计数据
+
+代码并不多:
+
+
+
+
+功能也比较简单:
+
+
+
+
+我们先从两个`Base`类开始看,分别是`BaseView`以及`BasePresenter`类。
+
+`BaseView`类:
+
+```java
+public interface BaseView+ * For simplicity, this implements a dumb synchronisation between locally persisted data and data + * obtained from the server, by using the remote data source only if the local database doesn't + * exist or is empty. + */ +public class TasksRepository implements TasksDataSource { + .... +} +``` + +先看一下`TasksDataSource`接口: + +```java +/** + * Main entry point for accessing tasks data. + *
+ * For simplicity, only getTasks() and getTask() have callbacks. Consider adding callbacks to other
+ * methods to inform the user of network/database errors or successful operations.
+ * For example, when a new task is created, it's synchronously stored in cache but usually every
+ * operation on database or network should be executed in a different thread.
+ */
+public interface TasksDataSource {
+
+ interface LoadTasksCallback {
+
+ void onTasksLoaded(List
+
+
+相对于传统布局`ConstraintLayout`在以下方面提供了一些新的特性:
+
+- 相对定位
+
+ 这个和`RelativeLayout`比较像,就是一个控件相对于另一个控件的位置约束关系:
+
+ - 横向:`Left、Right、Start、End`
+ - 纵向:`Top、Bottom、Baseline(文本底部的基准线)`
+
+ ```xml
+
+
+ ```
+ 常用的有:
+ ```xml
+ * layout_constraintLeft_toLeftOf // 左边左对齐
+ * layout_constraintLeft_toRightOf // 左边右对齐
+ * layout_constraintRight_toLeftOf // 右边左对齐
+ * layout_constraintRight_toRightOf // 右边右对齐
+ * layout_constraintTop_toTopOf // 上边顶部对齐
+ * layout_constraintTop_toBottomOf // 上边底部对齐
+ * layout_constraintBottom_toTopOf // 下边顶部对齐
+ * layout_constraintBottom_toBottomOf // 下边底部对齐
+ * layout_constraintBaseline_toBaselineOf // 文本内容基准线对齐
+ * layout_constraintStart_toEndOf // 起始边向尾部对齐
+ * layout_constraintStart_toStartOf // 起始边向起始边对齐
+ * layout_constraintEnd_toStartOf // 尾部向起始边对齐
+ * layout_constraintEnd_toEndOf // 尾部向尾部对齐
+
+ ```
+
+ 上面的这些属性需要结合`id`才能进行约束,这些id可以指向控件也可以指向父容器(也就是`ConstraintLayout`),比如:
+ ```xml
+
+ ```
+
+- 外边距
+
+ ```xml
+ * android:layout_marginStart
+ * android:layout_marginEnd
+ * android:layout_marginLeft
+ * android:layout_marginTop
+ * android:layout_marginRight
+ * android:layout_marginBottom
+ // 这里的gone margin指的是B向A添加约束后,如果A的可见性变为GONE,这时候B的外边距可以改变,也就是B的外边距根据A的可见性分为两种状态。
+ * layout_goneMarginStart
+ * layout_goneMarginEnd
+ * layout_goneMarginLeft
+ * layout_goneMarginTop
+ * layout_goneMarginRight
+ * layout_goneMarginBottom
+
+ ```
+
+- 居中和倾向
+ - 居中
+ 在`RelativeLayout`中我们可以`centerHorizontal`等来进行居中操作,但是在`ConstraintLayout`中没有类似的方法。
+ ```xml
+ > {
+ val result = ArrayList
>()
+ val size = nums.size
+
+ if (nums.size < 3) {
+ return result
+ }
+ nums.sort()
+
+
+ for (i in nums.indices) {
+ // 剪枝:最小的数已经 > 0,后面不可能凑出和为 0,直接break
+ if (nums[i] > 0) {
+ break
+ }
+
+ // 对固定数去重:跳过与前一个相同的值(注意是 i-1,不是 i+1)
+ if (i > 0 && nums[i] == nums[i - 1]) {
+ continue
+ }
+ val target = - nums[i]
+
+ var left = i + 1
+ var right = size - 1
+
+ while (left < right) {
+ val sum = nums[left] + nums[right]
+ if (sum == target) {
+ result.add(listOf(nums[i], nums[left], nums[right]))
+
+ // 左指针去重
+ while (left < right && nums[left] == nums[left + 1]) {
+ left++
+ }
+ // 右指针去重
+ while (left < right && nums[right] == nums[right - 1]) {
+ right--
+ }
+
+ // 收缩双指针,继续寻找下一组
+ left++
+ right--
+ } else if (sum < target) {
+ left ++
+ } else {
+ right --
+ }
+ }
+ }
+
+ return result
+ }
+}
+```
+
+---
+
+
+
+
+
+```python
+
+class Solution:
+ def threeSum(self, nums: List[int]) -> List[List[int]]:
+ res = []
+ if not nums or len(nums) < 3:
+ return res
+ nums = sorted(nums)
+ for i in range(len(nums) - 2):
+ if nums[i] > 0:
+ return res
+ if i > 0 and nums[i] == nums[i - 1]:
+ continue
+
+ left = i + 1
+ right = len(nums) - 1
+ while left < right:
+ result = nums[i] + nums[left] + nums[right]
+ if result > 0:
+ # 需要right减小
+ right -= 1
+ elif result < 0:
+ left += 1
+ else:
+ res.append([nums[i], nums[left], nums[right]])
+ # 去重复,判断左边或右边的元素是否与当前的相同,相同就夸脱
+ while left < right and nums[left] == nums[left + 1]:
+ left += 1
+
+ while left < right and nums[right] == nums[right - 1]:
+ right -= 1
+ # 移动指针
+ left += 1
+ right -= 1
+ return res
+
+```
+
+
+复杂度分析:
+
+- 时间复杂度:O(n²),数组排序 O(NlogN),遍历数组 O(n),双指针遍历 O(n),总体 O(NlogN)+O(n)∗O(n),O(n²)
+- 空间复杂度:O(1)
+
+
+
+- [下一题](https://github.com/CharonChui/AndroidNote/blob/master/Algorithm/30.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.md)
+
+
+---
+- 邮箱 :charon.chui@gmail.com
+- Good Luck!
+
+
diff --git "a/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" "b/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md"
new file mode 100644
index 00000000..b89c5200
--- /dev/null
+++ "b/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md"
@@ -0,0 +1,102 @@
+3.删除有序数组中的重复项
+===
+
+
+### 题目
+
+给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
+
+考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
+
+更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
+返回 k 。
+
+
+
+示例 1:
+
+输入:nums = [1,1,2]
+
+输出:2, nums = [1,2,_]
+
+解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
+
+
+示例 2:
+
+输入:nums = [0,0,1,1,1,2,2,3,3,4]
+
+输出:5, nums = [0,1,2,3,4]
+
+解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
+
+
+
+### 思路
+
+双指针
+- 既然非严格递增队列,那么重复的元素一定会相邻,那我们可以从0开始遍历,将没有重复的数值替换放到数组的开头
+
+
+```python
+class Solution:
+ def removeDuplicates(self, nums: List[int]) -> int:
+ # 第一个位置的不用判断
+ k = 1
+ for i in range(len(nums)):
+ # 当前的数值与已放置的数值不同就重新放置
+ if nums[i] != nums[k - 1]:
+ nums[k] = nums[i]
+ k += 1
+ return k
+```
+
+
+```kotlin
+class Solution {
+ fun removeDuplicates(nums: IntArray): Int {
+ var k = 1
+ for(i in nums){
+ if (i != nums[k - 1]) {
+ nums[k] = i
+ k ++
+ }
+ }
+ return k
+ }
+}
+```
+
+优化:
+
+假如数组是[0, 1, 2, 3, 4, 5]
+此时数组中没有重复元素,按照上面的方法,每次比较时nums[i]都不等于nums[k - 1],因此就会将k指向的元素原地复制一遍,这个操作其实是不必要的。
+
+
+优化的核心思想是:只有当当前索引 i 和写入位置 k 不相等时,才进行复制操作。
+
+所以优化就是:只有当 i > k 时才写入
+
+```python
+def removeDuplicates(self, nums: List[int]) -> int:
+ k = 1
+ for i in range(len(nums)):
+ if nums[i] != nums[k - 1]:
+ if i > k:
+ nums[k] = nums[i]
+ k += 1
+ return k
+```
+
+一定要注意: 这里比较的是 nums[i] != nums[k -1], 而不能是i - 1。
+
+因为i中的元素会被后面的覆盖掉,会变。 比较i - 1是不对的,而k - 1才是去重后的真实结果。
+
+
+- [下一篇](https://github.com/CharonChui/AndroidNote/blob/master/Algorithm/4.%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E9%A1%B9II.md)
+
+---
+- 邮箱 :charon.chui@gmail.com
+- Good Luck!
+
+
diff --git "a/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md"
new file mode 100644
index 00000000..343c6eb6
--- /dev/null
+++ "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md"
@@ -0,0 +1,213 @@
+30.长度最小的子数组
+===
+
+
+### 题目
+
+给定一个含有 n 个正整数的数组和一个正整数 target 。
+
+找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
+
+
+
+示例 1:
+
+- 输入:target = 7, nums = [2,3,1,2,4,3]
+- 输出:2
+- 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
+
+示例 2:
+
+- 输入:target = 4, nums = [1,4,4]
+- 输出:1
+
+示例 3:
+
+- 输入:target = 11, nums = [1,1,1,1,1,1,1,1]
+- 输出:0
+
+
+提示:
+
+- 1 <= target <= 109
+- 1 <= nums.length <= 105
+- 1 <= nums[i] <= 104
+
+
+进阶:
+
+如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
+
+
+
+### 思路
+
+
+#### 暴力循环(O(n²))
+
+枚举每个启动l,向右累加直到 >= target,记录最短长度,这能过,但不是最优。
+
+问题在于每次换起点都要从头累加,做了大量重复计算。
+
+
+#### 滑动窗口(O(n))
+
+什么是滑动窗口?
+
+滑动窗口是一种处理连续区间问题的算法技巧。
+
+它维护一个由两个指针(left和right)框定的窗口,这个窗口在数据上从左向右滑动,通过动态调整窗口的两个边界,避免重复计算,把很多O(n²)的暴力问题优化到O(n)。
+
+
+
+
+为什么能用滑动窗口?
+
+关键在于题目条件: 全是正整数,这意味着:
+
+- 窗口右边界right右移(会加入新元素),窗口的和只会增加不会减小。
+
+- 窗口左边界left右移(会移除元素),窗口的和只会减小不会增加。
+
+- 两条边都只能往右走,从不回头,正是因为两个指针不回头,每个元素最多被right碰一次、被left碰一次,总共2n次操作,所以复杂度为O(n)
+
+
+
+滑动窗口能成立的要求: 单调性
+
+这是最本质的原理,滑动窗口能用的前提是窗口的某个属性随边界移动而单调变化。
+
+
+
+
+
+
+这种单调性是滑动窗口成立的前提。
+
+如果数组含有负数,加入元素不一定变大,移除也不一定变小,滑动窗口就失效了。
+
+
+核心思想:
+
+
+维护一个[left, right]的窗口,用一个变量sum记录窗口内元素之和:
+
+- 扩张窗口: right不断右移,把nums[right]加入sum
+- 收缩窗口: 一旦sum >= target,说明当前窗口满足条件,此时尝试缩小窗口 -- 记录当前长度,然后把nums[left]移出、left ++,看能不能在仍满足 >= target 的前提下变得更短
+
+- 用while而非if来收缩,因为可能连续移出多个元素仍然能满足条件。
+
+
+
+```kotlin
+class Solution {
+ fun minSubArrayLen(target: Int, nums: IntArray): Int {
+ // 注意这里必须是MAX_VALUE,因为如果你是0,在那下面min的时候就始终一直是0
+ var result = Int.MAX_VALUE
+
+ var leftIndex = 0
+ var windowSum = 0
+ for (rightIndex in nums.indices) {
+ windowSum += nums[rightIndex]
+
+ while (windowSum >= target) {
+ result = min(result, rightIndex - leftIndex + 1)
+ windowSum -= nums[leftIndex]
+ leftIndex ++
+ }
+ }
+
+ return if (result == Int.MAX_VALUE) { 0 } else { result }
+ }
+}
+```
+
+
+
+复杂度分析:
+
+- 时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。
+
+- 空间复杂度:O(1)。
+
+
+
+
+#### 进阶要求解法: 前缀和 + 二分查找 (O(nlog n))
+
+
+
+- 构造前缀和数组 prefix,其中 prefix[i] = nums[0] + ... + nums[i-1](prefix[0] = 0)
+
+- 子数组 [l, r] 的和 = prefix[r+1] - prefix[l]
+
+- 对每个左端点 i,我们要找最小的 j,使得 prefix[j] - prefix[i] >= target,即 prefix[j] >= prefix[i] + target
+
+- 因为全是正整数,prefix 严格递增(有序),所以可以用二分查找快速定位最小的 j。
+
+```kotlin
+class Solution {
+ fun minSubArrayLen(target: Int, nums: IntArray): Int {
+ val n = nums.size
+ // prefix[i] 表示前 i 个元素之和;prefix[0] = 0
+ // 这里为什么size要是n+1?
+ // 考虑从下标 0 开始的子数组 [0, r],它的和 = prefix[r+1] - prefix[0]。
+ // 这里必须用到 prefix[0]。如果 prefix[0] 不存在,你就没法表示「从第一个元素开始」的子数组的和。
+ val prefix = IntArray(n + 1)
+ for (i in 1..n) {
+ prefix[i] = prefix[i - 1] + nums[i - 1]
+ }
+
+ var minLen = Int.MAX_VALUE
+ for (i in 0..n) {
+ // 要找最小的 j,使 prefix[j] >= prefix[i] + target
+ val needed = prefix[i] + target
+ val bound = lowerBound(prefix, needed)
+ if (bound != -1) {
+ minLen = minOf(minLen, bound - i)
+ }
+ }
+
+ return if (minLen == Int.MAX_VALUE) 0 else minLen
+ }
+
+ // 在升序数组中找第一个 >= key 的下标,找不到返回 -1
+ private fun lowerBound(arr: IntArray, key: Int): Int {
+ var lo = 0
+ var hi = arr.size // 左闭右开 [lo, hi)
+ var result = -1
+ while (lo < hi) {
+ val mid = lo + (hi - lo) / 2
+ if (arr[mid] >= key) {
+ result = mid // 记录候选,继续往左找更小下标
+ hi = mid
+ } else {
+ lo = mid + 1
+ }
+ }
+ return result
+ }
+}
+```
+
+
+
+
+复杂度分析:
+
+- 时间复杂度:O(nlog n)。
+
+- 空间复杂度:O(n) 前缀和数组。
+
+
+---
+
+- [下一题](https://github.com/CharonChui/AndroidNote/blob/master/Algorithm/31.%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2.md)
+
+
+
+---
+- 邮箱 :charon.chui@gmail.com
+- Good Luck!
+
+
diff --git "a/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" "b/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md"
new file mode 100644
index 00000000..66639c62
--- /dev/null
+++ "b/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md"
@@ -0,0 +1,174 @@
+31.无重复字符的最长子串
+===
+
+
+### 题目
+
+给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
+
+
+
+示例 1:
+
+- 输入: s = "abcabcbb"
+- 输出: 3
+- 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
+
+示例 2:
+
+- 输入: s = "bbbbb"
+- 输出: 1
+- 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
+
+示例 3:
+
+- 输入: s = "pwwkew"
+- 输出: 3
+- 解释:
+ - 因为无重复字符的最长子串是 "wke",所以其长度为 3。
+ - 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
+
+
+提示:
+
+- 0 <= s.length <= 5 * 104
+- s 由英文字母、数字、符号和空格组成
+
+
+### 思路
+
+这道题主要用到思路是:滑动窗口
+
+什么是滑动窗口?
+
+其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!
+
+如何移动?
+
+我们只要把队列的左边的元素移出就行了,直到满足题目要求!
+
+一直维持这样的队列,找出队列出现最长的长度时候,求出解!
+
+
+
+```python
+class Solution:
+ def lengthOfLongestSubstring(self, s: str) -> int:
+ leftIndex = 0
+ rightIndex = 0
+
+ # pwwkew
+ # "abcabcbb"
+ result = 0
+ while rightIndex < len(s):
+ index = s[leftIndex: rightIndex].find(s[rightIndex])
+ if index >= 0:
+ leftIndex += index + 1
+ rightIndex += 1
+ result = max(result, rightIndex - leftIndex)
+ return result
+```
+
+这个实现的时间复杂度是O(n²),显然不满足。
+
+可以使用Set或者HashMap进行优化:
+
+
+
+```python
+class Solution:
+ def lengthOfLongestSubstring(self, s: str) -> int:
+ leftIndex = 0
+ rightIndex = 0
+ result = 0
+ data = set()
+ while rightIndex < len(s):
+ while s[rightIndex] in data:
+ data.remove(s[leftIndex])
+ leftIndex += 1
+ data.add(s[rightIndex])
+ rightIndex += 1
+ result = max(result, rightIndex - leftIndex)
+ return result
+```
+
+
+
+---
+
+```kotlin
+class Solution {
+ fun lengthOfLongestSubstring(s: String): Int {
+ val window = HashSet