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,
+ int requestCode)`方法来申请对用的权限。参数传递对应所需的权限以及一个整数型的`request code`来标记该权限申请。 该方法是异步的:该方法会立即返回,在用户响应了请求权限的对话框之后,系统会调用对用的回调方法来通知结果,并且会传递在`reqeustPermissions()`方法中的`request code`。(在Android 6.0之前调用的时候会直接去调用`onRequestPermissionsResult()`的回调方法)
+如图:
+
+
+
+下面是检查是否读取联系人权限,并且在必要时申请权限的代码:
+
+```java
+// Here, thisActivity is the current activity
+if (ContextCompat.checkSelfPermission(thisActivity,
+ Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ // Should we show an explanation?
+ if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
+ Manifest.permission.READ_CONTACTS)) {
+
+ // Show an expanation to the user *asynchronously* -- don't block
+ // this thread waiting for the user's response! After the user
+ // sees the explanation, try again to request the permission.
+
+ } else {
+
+ // No explanation needed, we can request the permission.
+
+ ActivityCompat.requestPermissions(thisActivity,
+ new String[]{Manifest.permission.READ_CONTACTS},
+ MY_PERMISSIONS_REQUEST_READ_CONTACTS);
+
+ // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
+ // app-defined int constant. The callback method gets the
+ // result of the request.
+ }
+}
+```
+
+> 注意:当调用`requestPermissions()`方法时,系统会显示一个标准的对话框。应用不能指定或者改变该对话框。如果你想提供一些信息或者说明给用户,你需要在调用`requestPermissions()`之前处理。
+
+##### 处理请求权限的的结果
+
+如果应用申请权限,系统会显示一个对话框。当用户相应后,系统会调用应用中的`onRequestPermissionsResult (int requestCode,
+ String[] permissions,
+ int[] grantResults)`方法并传递用户的操作结果。在应用中必须要重写该方法来查找授权了什么权限。该回调方法会传递你在`requestPermisssions()`方法中传递的`request code`。直接在`Activity`或者`Fragment`中重写`onRequestPermissionsResult()`方法即可。例如,申请`READ_CONTACTS`的权限可能会有下面的回到方法:
+
+```java
+@Override
+public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ switch (requestCode) {
+ case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+
+ // permission was granted, yay! Do the
+ // contacts-related task you need to do.
+
+ } else {
+
+ // permission denied, boo! Disable the
+ // functionality that depends on this permission.
+ }
+ return;
+ }
+
+ // other 'case' lines to check for other
+ // permissions this app might request
+ }
+}
+
+```
+
+系统提示的对话框会描述应用所需的`permission groud`。它不会列出特定的权限。例如,如果你申请了`READ_CONTACTS`权限,系统的对话框只会说你的应用需要获取设备的联系人信息。用户只需要授权每个`permission group`一次。如果你应用需要申请其他任何一个在该`permission group`中的权限时,系统会自动授权。在申请这些授权时,系统会像用户明确通过系统对话框统一授权时一样去调用`onRequestPermissionsResult()`方法并且传递`PERMISSION_GRANTED`参数。
+
+> 注意:虽然用户已经授权了同一`permission group`中其他的任何权限,但是应用仍然需要明确申请每个需要的权限。例外,`permission group`中的权限在以后可能会发生变化。
+
+例如,假设在应用的`manifest`文件中同时声明了`READ_CONTACTS`和`WRITE_CONTACTS`权限。如果你申请`READ_CONTACTS`权限而且用户同意了该权限,如果你想继续申请`WRITE_CONTACTS`权限,系统不会与用户有任何交互就会直接进行授权。
+
+如果用户拒绝了一个权限申请,你的应用进行合适的处理。例如,你的应用可能显示一个对话框来表明无法执行用户请求的需要该权限的操作。
+
+如果系统向用户申请权限授权,用户选择了让系统以后不要再申请该权限。 在这种情况下,应用在任何时间调用`reqeustPermissions()`方法来再次申请权限时,系统都会直接拒绝该请求。系统会直接调用`onRequestPermissionResult()`回调方法并且传递`PERMISSION_DENIED`参数,和用户明确拒绝应用申请该权限时一样。 这就意味着在你调用`requestPermissions()`方法是,你无法确定是否会和用户有直接的交互操作。
+
+
+示例代码:
+```java
+
+final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
+
+private void insertDummyContactWrapper() {
+ List
+
+不得不说,`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 This can be useful for applications that wish to implement various forms of gestural
+ * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
+ * a touch interaction already in progress even if the RecyclerView is already handling that
+ * gesture stream itself for the purposes of scrolling.
+
+
+相对于传统布局`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