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/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" similarity index 99% rename from "AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" rename to "AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" index 418931fc..1abe582f 100644 --- "a/AdavancedPart/\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260.md" +++ "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" @@ -1,5 +1,5 @@ -热修复实现 +热修复实现(一) === @@ -18,7 +18,7 @@ 在一般情况下,应用程序不需要创建`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`中提供的D`exClassLoader`类就是出于这个目的。 +对于`Android`的应用程序,本质上虽然也是用`Java`开发,并且使用标准的`Java`编译器编译出`Class`文件,但最终的`APK`文件中包含的却是`dex`类型的文件。`dex`文件是将所需的所有`Class`文件重新打包,打包的规则不是简单的压缩,而是完全对`Class`文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是`dex`文件。由于`dex`文件是一种经过优化的`Class`文件,因此要加载这样特殊的`Class`文件就需要特殊的类装载器,这就是`DexClassLoader`,`Android SDK`中提供的`DexClassLoader`类就是出于这个目的。 总体来说,`Android` 默认主要有三个`ClassLoader`: @@ -75,7 +75,7 @@ public class DexClassLoader extends BaseDexClassLoader { } ``` 注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, -因为家在都是从`loadClass()`方法中,所以我们去`ClassLoader`类中看一下`loadClass()`方法: +因为加载都是从`loadClass()`方法中,所以我们去`ClassLoader`类中看一下`loadClass()`方法: ```java /** * Loads the class with the specified name. Invoking this method is @@ -128,7 +128,7 @@ public class DexClassLoader extends BaseDexClassLoader { if (clazz == null) { ClassNotFoundException suppressed = null; try { - // 先检查父ClassLoader是否已经家在过该类 + // 先检查父ClassLoader是否已经加载过该类 clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; @@ -441,7 +441,7 @@ public class PluginClass { ``` 现在如果想调用插件`APK`中`PluginClass`内的方法,应该怎么办? -####从已安装的apk中读取dex +#### 从已安装的apk中读取dex 先来看第一种方法,这种方法必须建一个`Activity`,在清单文件中配置`Action`. @@ -513,7 +513,7 @@ public void btnClick(View view){ } ``` -####从apk文件中读取dex +#### 从apk文件中读取dex 这种方法由于并不需要安装,所以不需要通过`Intent`从`activity`中解析信息。换言之,这种方法不需要创建`Activity`。无需配置清单文件。我们只需要打包一个`apk`,然后放到`SD`卡中即可。 核心代码如下: diff --git "a/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" new file mode 100644 index 00000000..7d02b934 --- /dev/null +++ "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" @@ -0,0 +1,97 @@ + +热修复实现(二) +=== + +之前也分析过`InstantRun`的源码,前面也写了一篇热修复实现原理的文章。 + +But,最近遇到困难了,所在项目要做插件化,同事在开发过程中遇到了一个在5.0以下手机崩溃的问题,想着一起找找原因修复下。 +但是这个bug已经折腾了我两天了,只找到原因,并没有找出任何解决方案。 + +深深的感觉到之前的那些都是皮毛,没有真正的去做,真正的去处理一些细节,那些都是没任何意义的。 + +所以这里系统学习一下,既然学习,就找一个做的最好的来学。 + + +前面的文章也介绍了,目前存在的几个开源框架: + +- 手机淘宝基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术-Dexposed。 +但这个方案犹豫底层Dalvik结构过于依赖,最终无法兼容Android 5.0以后的ART虚拟机。 + +- 支付宝提出了新的方案Andfix,它同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,而且做到了Dalvik和ART全版本的兼容。然而它也 +是由局限性的,且不说其底层固定结构的替换方案稳定性不好,其使用范文也存在着诸多限制,虽然可以通过代码改造来绕过限制达到同样的 +修复目的,但是这种方式即不优雅也不方便。而且更大的问题是Andfix只提供了代码层面的修复,对于资源和so的修复都还未实现。 + +- 其他的就是微信Tinker、饿了么的Amigo、美团的Robust,不过他们都各自有各自的局限性,或者不够稳定、或者补丁过大、或者效率低下 +,或者使用起来太繁琐,大部分技术上看起来似乎可行,但是实际体验并不好。 + +我们学习的就是阿里巴巴的新一代非侵入式Android热修复方案-Sophix。 +它各个方面都比较优秀,使用也比较方便,唯一不支持的就是四大组件的修复。这是因为如果修复四大组件,必须在AndroidManifest里面预先插入代码组件,并且尽可能声明所有权限,这样就会给 +原先的app添加很多臃肿的代码,对app运行流程的侵入性很强。 + + +在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不需要做任何修改。 + +这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使的补丁工具操作图形界面化,这种所见即所得的补丁生成 +方式也是阿里热修复独家的,因此,Sophix的接入成本也是目前市面上所有方案里最低的。 + + + + +代码修复 +--- + +代码修复有两大主要方案: + +- 阿里系的底层替换方案:底层替换方案限制颇多,但是时效性最好,加载轻快,立即见效。 + + 底层替换方案是在已经加载了的类中直接替换掉原有的方法,是在原来类的基础上进行修改的。因而无法实现对原有类进行方法和字段的增减,因为这样将破坏原有类的结构。 + 一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个dex的方法数的变化,方法数的变化伴随着方法索引的变化,这样在 + 访问方法时就无法正常的索引到正确的方法了。如果字段发生了增加或减少,和方法变化的情况一样,所有字段的索引都会发生变化。 + 而新方法中使用到这些老的示例对象时,访问新增字段就会会产生不可预期的结果。 + + 这是该类方案的固有限制,而底层替换方案最为人逅病的地方,在于底层替换的不稳定性。因为Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。因为Art虚拟机和Dalvik虚拟机的不同,每个版本的虚拟机都要适配,而且Android系统是开源的,各个厂商都可以对代码进行改造,如果某个厂商进行了修改,那么这种通用性的替换机制就会出问题。这便是不稳定的根源。 + + + +- 腾讯系的类加载方案:类加载方案时效性差,需要重新冷启动才能见效,但修复范围广、限制少。 + +类加载方案的原理是在app重新启动后让classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类,因此只有在下次重启的时候, +在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类的时候,就会用新类,从而达到热修复的目的。 + + +为什么sophix更好? +--- + +既然底层替换方案和类加载方案各有优点,把他们联合起来就是最好的选择。Sophix就是同时涵盖了这两种方案,可以实现优势互补、完全兼顾的作用,可以灵活的根据实际情况自动切换。 + + + + + + +但是[Sophix](https://help.aliyun.com/document_detail/57064.html?spm=a2c4g.11186623.6.543.SPhMhO)有一个缺点就是,他不是开源的,而且是收费的。但是确实强大。 + + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [Android 热补丁动态修复框架小结](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [美团Android DEX自动拆包及动态加载简介](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [插件化从放弃到捡起](http://kymjs.com/column/plugin.html) +- [无需Root也能使用Xposed!](http://weishu.me/) +- [当你准备开发一个热修复框架的时候,你需要了解的一切](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md "InstantRun详解" + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" new file mode 100644 index 00000000..4da9dd5c --- /dev/null +++ "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" @@ -0,0 +1,395 @@ +在做热修复功能时Java层通过反射调用addAssetPath在Android5.0及以上系统没有问题,在Android 4.x版本找不到资源。 + +addAssetPath方法: +```java +/** + * Add an additional set of assets to the asset manager. This can be + * either a directory or ZIP file. Not for use by applications. Returns + * the cookie of the added asset, or 0 on failure. + * {@hide} + */ +public final int addAssetPath(String path) { + int res = addAssetPathNative(path); + return res; +} +private native final int addAssetPathNative(String path); +``` + +addAssetPath具体的实现方法是native层的。 + +[AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1.0.1/libs/androidfw/AssetManager.cpp) + +4.4及以前的版本调用addAssetPath方法时,只是把补丁包的路径添加到mAssetPath中,不会去重新解析,真正解析的代码是在app第一次执行AssetManager.getResTable()`方法的时候。 +一旦解析完一次后,mResource对象就不为nil,以后就会直接return掉,不会重新解析。 + +```c +bool AssetManager::addAssetPath(const String8& path, void** cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(this)->loadFileNameCacheLocked(); + const size_t N = mAssetPaths.size(); + // 真正解析package的地方 + for (size_t i=0; i(this)-> + mZipSet.getZipResourceTable(ap.path); + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (i == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, (void*)(i+1), false, idmap); + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + Asset* ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + if (rt == NULL) { + mResources = rt = new ResTable(); + updateResourceParamsLocked(); + } + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + rt->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + rt->add(ass, (void*)(i+1), !shared, idmap); + } + if (!shared) { + delete ass; + } + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + } + if (required && !rt) ALOGW("Unable to find resources file resources.arsc"); + if (!rt) { + mResources = rt = new ResTable(); + } + return rt; +} + + +const ResTable& AssetManager::getResources(bool required) const +{ + const ResTable* rt = getResTable(required); + return *rt; +} +``` +而当我们执行加载补丁的代码的时候,getResTable已经执行过多次了,Android Framework里面的代码会多次调用该方法。所以即使是使用addAssetPath,也只是添加到了mAssetPath,并不会发生解析,所以补丁包里面的资源就是完全不生效的。 + +而在android 5.0及以上的代码中: + +[Android 5.0 AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r7/libs/androidfw/AssetManager.cpp) + +```c +bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(i+1); + } + return true; + } + } + ALOGV("In %p Asset %s path: %s", this, + ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); + // Check that the path has an AndroidManifest.xml + Asset* manifestAsset = const_cast(this)->openNonAssetInPathLocked( + kAndroidManifest, Asset::ACCESS_BUFFER, ap); + if (manifestAsset == NULL) { + // This asset path does not contain any resources. + delete manifestAsset; + return false; + } + delete manifestAsset; + mAssetPaths.add(ap); + // new paths are always added at the end + if (cookie) { + *cookie = static_cast(mAssetPaths.size()); + } +#ifdef HAVE_ANDROID_OS + // Load overlays, if any + asset_path oap; + for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { + mAssetPaths.add(oap); + } +#endif + if (mResources != NULL) { + // 重新调用该方法去解析package + appendPathToResTable(ap); + } + return true; +} +``` + +```c +bool AssetManager::appendPathToResTable(const asset_path& ap) const { + Asset* ass = NULL; + ResTable* sharedRes = NULL; + bool shared = true; + bool onlyEmptyResources = true; + MY_TRACE_BEGIN(ap.path.string()); + Asset* idmap = openIdmapLocked(ap); + size_t nextEntryIdx = mResources->getTableCount(); + ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); + if (ap.type != kFileTypeDirectory) { + if (nextEntryIdx == 0) { + // The first item is typically the framework resources, + // which we want to avoid parsing every time. + sharedRes = const_cast(this)-> + mZipSet.getZipResourceTable(ap.path); + if (sharedRes != NULL) { + // skip ahead the number of system overlay packages preloaded + nextEntryIdx = sharedRes->getTableCount(); + } + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (nextEntryIdx == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, idmap, nextEntryIdx + 1, false); +#ifdef HAVE_ANDROID_OS + const char* data = getenv("ANDROID_DATA"); + LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); + String8 overlaysListPath(data); + overlaysListPath.appendPath(kResourceCache); + overlaysListPath.appendPath("overlays.list"); + addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); +#endif + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + mResources->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + mResources->add(ass, idmap, nextEntryIdx + 1, !shared); + } + onlyEmptyResources = false; + if (!shared) { + delete ass; + } + } else { + ALOGV("Installing empty resources in to table %p\n", mResources); + mResources->addEmpty(nextEntryIdx + 1); + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + return onlyEmptyResources; +} +const ResTable* AssetManager::getResTable(bool required) const +{ + ResTable* rt = mResources; + if (rt) { + return rt; + } + // Iterate through all asset packages, collecting resources from each. + AutoMutex _l(mLock); + if (mResources != NULL) { + return mResources; + } + if (required) { + LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager"); + } + if (mCacheMode != CACHE_OFF && !mCacheValid) { + const_cast(this)->loadFileNameCacheLocked(); + } + mResources = new ResTable(); + updateResourceParamsLocked(); + bool onlyEmptyResources = true; + const size_t N = mAssetPaths.size(); + // 也是调用appendPathToResTable去解析 + for (size_t i=0; i │ .class │ ──────────────> │ 织入后.class │ + └─────────────┘ └─────────────┘ └──────────────┘ + ↑ + ┌─────────────┐ + │ Aspect.java │ + └─────────────┘ +``` + +ajc(AspcetJ Compiler)会: + +1. 扫描所有.class,找匹配切点的位置 +2. 按通知类型,在该位置直接修改字节码插入代码 +3. 输出新的.class + +这意味着: AOP的开销在编译期,运行期几乎无额外开销(仅多几行字节码)。 + + + + + + + + + diff --git "a/AdavancedPart/ART\344\270\216Dalvik.md" "b/AdavancedPart/ART\344\270\216Dalvik.md" deleted file mode 100644 index 4158d194..00000000 --- "a/AdavancedPart/ART\344\270\216Dalvik.md" +++ /dev/null @@ -1,41 +0,0 @@ -ART与Dalvik -=== - -ART ---- - -`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。 -`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地, -这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。 -`ART`代表`Android Runtime`,其处理应用程序执行的方式完全不同于`Dalvik`,`Dalvik`是依靠一个`Just-In-Time` (`JIT`)编译器去解释字节码。 -开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。 -`ART`则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫`Ahead-Of-Time`(`AOT`)编译。 -在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 - -Dalvik ---- - -`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行, -`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例, -并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 - -`AOT`的编译器分两种模式: -- 在开发机上编译预装应用; -- 在设备上编译新安装的应用,在应用安装时将dex字节码翻译成本地机器码。 - -ART优点: -- 系统性能的显著提升。 -- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。 -- 更长的电池续航能力。 -- 支持更低的硬件。 - -ART缺点: -- 更大的存储空间占用,可能会增加10%-20%。 -- 更长的应用安装时间。 - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git a/AdavancedPart/ARouter.md b/AdavancedPart/ARouter.md new file mode 100644 index 00000000..f9d9cc9f --- /dev/null +++ b/AdavancedPart/ARouter.md @@ -0,0 +1,71 @@ +ARouter +--- + + +[ARouter](https://github.com/alibaba/ARouter) +一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦 + + + + +```java +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivity(intent); + +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivityForResult(intent, 666); +``` +在未使用ARouter路由框架之前的原生页面跳转方式。 + + +## 原生路由方案的缺点 + +1. 显式: 直接的类依赖,耦合严重 +2. 隐式: 会在Manifest文件中进行集中管理,写作困难 + + +## ARouter的优势 + +1. 使用注解,实现了映射关系自动注册与分布式路由管理 +2. 编译期间处理注解,并生成映射文件,没有使用反射,不影响运行时性能 +3. 映射关系按组分类,分级管理,按需初始化。 +4. 灵活的降级策略,每次跳转都会回调跳转结果,避免startActivity()一旦失败会抛出异常 +5. 自定义拦截器,自定义拦截顺序,可以对路由进行拦截,比如登录判断和埋点处理 +6. 支持依赖注入,可单独作为依赖注入框架使用,从而实现跨模块API调用 +7. 支持直接解析标准url进行跳转,并自动注入参数到目标页面中 +8. 支持多模块使用,支持组件化开发 + + + +## 基本使用 + +添加依赖并初始化后的基本使用: + +```java +// 在支持路由的页面上添加注解(必选) +// 这里的路径需要注意的是至少需要有两级,/xx/xx +@Route(path = "/test/activity") +public class YourActivity extend Activity { + ... +} + +// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中) +ARouter.getInstance().build("/test/activity").navigation(); + +// 2. 跳转并携带参数 +ARouter.getInstance().build("/test/1") + .withLong("key1", 666L) + .withString("key3", "888") + .withObject("key4", new Test("Jack", "Rose")) + .navigation(); +``` + + + + + + + + diff --git a/AdavancedPart/Android WorkManager.md b/AdavancedPart/Android WorkManager.md new file mode 100644 index 00000000..17f51c57 --- /dev/null +++ b/AdavancedPart/Android WorkManager.md @@ -0,0 +1,173 @@ +Android WorkManager +=== + +谷歌在今年的`Google I/O`上宣布了一项非常令人兴奋的功能,该功能允许开发人员执行传统上需要详细了解各种API级别和可用于这些API的后台任务库的后台任务(简单点说就是”管理一些要在后台工作的任务, – 即使你的应用没启动也能保证任务能被执行”),这就是[WorkManager](https://developer.android.com/reference/androidx/work/WorkManager),`WorkManager`提供了从其他`API`(例如`JobScheduler`,`Firebase.JobDispatcher`,`AlarmManager`和`Services`)中获得的功能,而无需研究哪种`API`可用于您的设备或`API`。 + +这句话是什么意思?既然能用`JobScheduler`和`AlarmManager`等,何必再出来个`WorkManager`呢?其实`WorkManager`在底层也是看你是什么版本来选到底是`JobScheduler`,`AlamarManager`来做。 +`JobScheduler`是在`SDK`21中才有的,而且在`SDK`21中还有`Bug`,稳定可用是从`SDK`23开始的. 而`AlarmManager`一直存在,但是`AlarmManager`也不是最好的选择, +注意:如果您的应用程序的目标是`API`级别26或更高,则当应用程序本身不在前台时,系统会对运行后台服务施加限制。在大多数情况下,您的应用程序应该使用预定作业。所以`WorkManager`在底层, 会根据你的设备情况, 选用`JobScheduler`,`Firebase`的`JobDispatcher`,或是`AlarmManager`。 + + +`WorkManager`,它在应用被杀, 甚至设备重启后仍能保证你安排给他的任务能得到执行。那这样我们以后是不是可以把后台任务都用它来实现了?其实`Google`自己也说了:`WorkManager`并不是为了那种在应用内的后台线程而设计出来的. 这种需求你应该使用`ThreadPool`。 + +具体的规则如下: + +`WorkManager`根据以下标准在可用时使用对应的底层工作服务: + +- 在`API 23+`使用`JobScheduler` +- 在`API 14-22`中 + - 如果在应用中使用了`Firebase JobDispatcher`并且有可选的`Firebase`依赖项就使用`Firebase JobDispatcher` + - 否则使用自定义的`AlarmManager + BroadcastReceiver`的实现方式 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/workmanager.png) + + + + +组成部分: + +- `WorkManager`:通过对应的参数来接受`work`并排队执行`work`。 +- `Worker`:通过实现`doWork()`方法来指定在后台执行的具体功能。 +- `WorkRequest`:代表一个独立的任务。它会告诉你哪个`worker`加入了,以及它需要满足哪些约束才能运行。`WorkRequest`是一个抽象类,您将使用[OneTimeWorkRequest](https://developer.android.com/reference/androidx/work/OneTimeWorkRequest)或[PeriodicWorkRequest](https://developer.android.com/reference/androidx/work/PeriodicWorkRequest)。 +- `WorkStatus`:为每个WorkRequest对象提供数据。 + + + +好了下面开始演示: + +首先加入依赖: + +``` +dependencies { + def work_version = "1.0.0-alpha02" + implementation "android.arch.work:work-runtime:$work_version" +} +``` + +- 继承`Worker`类并实现`doWork()`方法 + +```kotlin +class MineWorker : Worker() { + override fun doWork(): WorkerResult { + Log.e("@@@", "dang dang dang !!!") + return WorkerResult.SUCCESS + } +} +``` + +- 接下来如果想要该`Worker`执行,需要调用`WorkManager`将该`Worker`添加到队列中 + +```kotlin +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val workRequest = OneTimeWorkRequest.Builder(MineWorker::class.java).build() + val instance = WorkManager.getInstance() + instance.enqueue(workRequest) + + } +} +``` + +好了,执行下: +``` +05-29 12:15:51.066 6360-6406/com.charon.workmanagerdemo E/@@@: dang dang dang !!! +``` + +并没什么卵用,因为这个示例代码就上来就执行一次。 很显然这个不合理啊,我想给该`worker`添加一些限制条件,例如我想在手机充电时,并且有网的情况下执行某个任务, +而且我想让它执行多次,这里可以通过`Constraints`来限制: + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresCharging(true) + .build() + + val workRequest = OneTimeWorkRequest.Builder(MineWorker::class.java) + .setConstraints(constraints).build() + + val instance = WorkManager.getInstance() + instance.enqueue(workRequest) +} +``` +我们把手机网络关掉运行下,这次运行后并没有`log`打印。那现在把网络打开: + +``` +05-29 12:21:02.061 7092-7241/? E/@@@: dang dang dang !!! +``` + + +当然我们可以获取每个请求的状态,以及取消该请求: +```kotlin +val statusById: LiveData = instance.getStatusById(workRequest.id) +instance.cancelWorkById(workRequest.id) +``` +如果想要顺序的去执行不同的`OneTimeWorker`也是很方便的: +```kotlin +WorkContinuation chain1 = WorkManager.getInstance() + .beginWith(workA) + .then(workB); +WorkContinuation chain2 = WorkManager.getInstance() + .beginWith(workC) + .then(workD); +WorkContinuation chain3 = WorkContinuation + .combine(chain1, chain2) + .then(workE); +chain3.enqueue(); +``` + +上面都是用了`OneTimeWorkRequest`,如果你想定期的去执行某一个`worker`的话,可以使用`PeriodicWorkRequest`: +```kotlin +val workRequest = PeriodicWorkRequest.Builder(MineWorker::class.java, 3, TimeUnit.SECONDS) + .setConstraints(constraints) + .setInputData(data) + .build() +``` + +但是我使用这个定期任务执行的时候也只是执行了一次,并没有像上面的代码中那样3秒执行一次,什么鬼? +我们看看`PeriodicWorkRequest`的源码: + +```kotlin +/** + * A class that represents a request for repeating work. + */ + +public final class PeriodicWorkRequest extends WorkRequest { + + /** + * The minimum interval duration for {@link PeriodicWorkRequest}, in milliseconds. + * Based on {@see https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/job/JobInfo.java#110}. + */ + public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes. +``` + +上面说的很明白,最小间隔15分钟,- -! + + +总体来说,`WorkManager`并不是要取代线程池`AsyncTask/RxJava`.反而是像`AlarmManager`来做定时任务的意思.即保证你给它的任务能完成, 即使你的应用都没有被打开, 或是设备重启后也能让你的任务被执行.`WorkManager`在设计上设计得比较好.没有把`worker`,任务混为一谈,而是把它们解耦成`Worker`,`WorkRequest`.这样分层就清晰多了, 也好扩展. + + +到了这里突然有了一个大胆的想法。看到没有它能保证任务的执行。 +我们之前写过一篇文章[Android卸载反馈](https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Android%E5%8D%B8%E8%BD%BD%E5%8F%8D%E9%A6%88.md) +里面用到了`c`中的`fork`来保证存活,达到常驻内存的功能,如果`PeriodicWorkRequest`的最小间隔时间比较短不是15分钟的话,那这里是不是也可以用`WorkManager`来实现? 好了,不说了。 + + + +参考: + +- [官方文档](https://developer.android.com/topic/libraries/architecture/workmanager) +- [Codelabs android-workmanager](https://codelabs.developers.google.com/codelabs/android-workmanager/#0) +- [示例代码](https://github.com/googlecodelabs/android-workmanager) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" "b/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" index 78e9bc3f..d7f32de9 100644 --- "a/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" +++ "b/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" @@ -103,7 +103,8 @@ int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, 为了能找到用户可能需要说明的情况,`android`提供了一个工具类方法`ActivityCompat.shouldShowRequestPermissionRationale().`。如果应用之前申请了该权限但是用户拒绝授权后该方法会返回`true`。(在Android 6.0之前调用的时候会直接返回false) > 注意:如果用户之前拒绝了权限申请并且选择了请求权限对话框中的`Don’t ask again`选项,该方法就会返回`false`。如果设备策略禁止了该应用使用该权限,该方法也会返回`false`。(我测试的时候发现请求权限的对话框中并没有`Don’t asdk again`这一项) -> ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/request_permission_dialog.png?raw=true) + + ##### 申请需要的权限 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处理。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dex_local_code.png) + +Android Runtime同样也回管理内存及垃圾回收。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_dvm.png) + + + +可以使用各种策略将字节码编译为机器代码,所有这些策略都有其取舍。为了了解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更具性能。一些经常使用的已编译代码段已被缓存,不再重新编译。但这是非常有限的,因为在最初的日子里内存非常稀缺。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dalvik_jit.png) + +几年来运行良好,但与此同时,手机的性能越来越高,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的次数、减少后台内存占用。 +在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dvm_art.png) + +`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可以预编译并缓存这些方法以获得最佳性能。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/art_profile_guide.png) + +该策略可为应用程序的关键部分提供最佳性能,同时减少RAM使用量。由于事实证明,对于大多数应用程序来说,通常仅使用10%到20%的代码。 + +更改ART之后,不再影响应用程序安装和系统更新的速度。应用的关键部分的预编译仅在设备空闲和充电时进行,以最大程度地减少对设备电池的影响。 + + + +这种方法的唯一缺点是,为了获取配置文件数据并预编译常用的方法和类,用户必须实际使用应用程序。这意味着该应用程序的一些首次使用可能会有点慢,因为在这种情况下,将仅使用即时编译。 + +这就是为什么要改善在Android P中的初始用户体验的原因,Google在云中引入了个人资料。 + +### Profiles in the cloud(Android P) + +云中的配置文件背后的主要思想是,大多数人以非常相似的方式使用该应用程序。因此,为了在安装后立即提高性能,我们可以从已经使用过此应用程序的人那里收集配置文件数据。此汇总的配置文件数据用于为应用程序创建一个称为通用核心配置文件的文件。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/profiles_in_cloud.png) + +因此,当新用户安装该应用程序时,该文件将与该应用程序一起下载。 ART使用它来预编译大多数用户经常运行的类和方法。这样,新用户在下载应用程序后即可获得更好的性能。 + +这并不意味着不再使用旧策略。用户运行应用程序后,ART将收集用户特定的配置文件数据并重新编译设备闲置时该特定用户经常使用的代码。 + +而最好的部分是,我们开发应用程序的开发人员无需执行任何操作即可启用此功能。这一切都发生在Android Runtime中。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_runtime_change.png) + + + +- 最初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/AdavancedPart/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" index 4b2e9aa4..5008840f 100644 --- "a/AdavancedPart/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`。 下面我们看一下示例图:  - ![image](https://github.com/CharonChui/Pictures/blob/master/singletask.gif?raw=true) + ![image](https://github.com/CharonChui/Pictures/blob/master/singletask.gif?raw=true) 坑爹啊!有木有!前面刚说`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键,这时所有返回栈都已经空了,也就自然退出了程序。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_launch_mode_singleinstance.png?raw=true) + + + + + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git a/AdavancedPart/ApplicationId vs PackageName.md b/AdavancedPart/ApplicationId vs PackageName.md index d70b2799..35dc2f0a 100644 --- a/AdavancedPart/ApplicationId vs PackageName.md +++ b/AdavancedPart/ApplicationId vs PackageName.md @@ -7,7 +7,7 @@ ApplicationId vs PackageName 在`Android`官方文档中有一句是这样描述`applicationId`的:`applicationId : the effective packageName`,真是言简意赅,那既然`applicationId`是有效的包明了,`packageName`算啥? -所有`Android`应用都有一个包名。包名在设备上能唯一的标示一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 +所有`Android`应用都有一个包名。包名在设备上能唯一的标识一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 之前的`Android Gradle`构建系统中,应用的包名是由你的`manifest`文件中的根元素中的`package`属性定义的: @@ -52,7 +52,7 @@ android { ... ``` -像之前一样,你需要在`Manifest`文件中指定你在代码中使用的'package name',像上面`AndroidManifest.xml`的例子。 +像之前一样,你需要在`Manifest`文件中指定你在代码中使用的`package name`,像上面`AndroidManifest.xml`的例子。 下面进入关键部分了:当你按照上面的方式做完后,这两个包就是相互独立的了。你现在可以很简单的重构你的代码-通过修改`Manifest`中的包名来修改在你的`activitise`和`services`中使用的包和在重构你在代码中的引用声明。这不会影响你应用的最终`id`,也就是在`Gradle`文件中的`applicationId`。 你可以通过以下`Gradle DSL`方法为应用的`flavors`和`build types`指定不同的`applicationId`: @@ -86,4 +86,4 @@ buildTypes { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" index 1485641a..0a6a7f1a 100644 --- "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" +++ "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -5,7 +5,7 @@ BroadcastReceiver安全问题 - 保证发送的广播要发送给指定的对象 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 -- 保证我接收到的广播室指定对象发送过来的 +- 保证我接收到的广播是指定对象发送过来的 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 `android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: @@ -46,4 +46,4 @@ protected void onDestroy() { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/Componse\344\275\277\347\224\250.md" "b/AdavancedPart/Componse\344\275\277\347\224\250.md" new file mode 100644 index 00000000..6a2f0449 --- /dev/null +++ "b/AdavancedPart/Componse\344\275\277\347\224\250.md" @@ -0,0 +1,130 @@ +Compose使用 +=== + +传统 Android UI 开发采用命令式模型,即通过命令驱动视图变化:findViewById 查找控件、设置属性、处理交互逻辑。代码经常伴随着多个职责耦合在一起,结构混乱,易错难测。 + +Compose 则采用声明式模型:界面即状态的函数表达。当状态改变时,对应的 Composable 自动重新组合(Recompose)并刷新界面。这种模式更贴近现代前端(如 React/Vue)的理念。 + +无需关心视图更新逻辑,只要状态变化,界面自然重绘,大幅降低 UI 层复杂度。 + + +1.3 开发效率提升点 + +代码量平均减少 40%-60% + +无需 ViewHolder、Adapter 逻辑 + +状态与 UI 同步更新,避免 UI 状态丢失 + +支持实时预览(@Preview)、热重载、即时调试 + + +我们在公司项目中构建了性能对比 Benchmark(测试设备为 Pixel 6、Android 13): + +2.1 滚动列表对比:RecyclerView vs LazyColumn + +指标 RecyclerView LazyColumn (Compose) 差异 +平均帧率 +48 fps +58 fps ++20% +内存占用 +28 MB +22 MB +-21% +首次绘制耗时 +320 ms +210 ms +-34% + + +2.2 原因解析:Compose 更快的秘密 + +SlotTable:结构快照树 +Compose 编译器会将 Composable 函数转换为组装 SlotTable 的代码。SlotTable 是一种高效的数据结构,存储了 Composable 树的结构快照。当状态发生变化时,Compose 通过对比 SlotTable 的版本,精确地定位变化范围,从而进行最小代价的重组操作(recomposition)。这一过程通过 Composer 对 Slot 表的操作实现,避免了冗余 UI 节点更新。 + +重组与 Group 管理机制 +Compose 使用 Group(startGroup/endGroup)对 Composable 调用进行打包与标识,每个重组区域会通过重新执行对应的 Group 来进行更新,确保仅变更部分被执行。此机制在 RecomposeScopeImpl 中有体现,它能追踪每个状态依赖的作用域,从而提升重组精度。 + +无需 ViewHolder 回收 +传统 RecyclerView 需要手动管理视图缓存与回收,而 Compose 自动处理 Composition 节点生命周期。Compose Compiler 会生成高效的 Slot 操作指令,通过“skip、reuse”策略对 UI 层进行精准控制,避免重复创建与销毁。 + +Skia 图形引擎与 RenderNode +Compose 绘制层基于 Skia 引擎,使用 DrawModifier 直接对 Canvas 进行渲染。它不会像传统 View 那样层层嵌套测量布局与绘制流程,而是采用测量(MeasurePass)-> 布局(LayoutPass)-> 绘制(DrawPass)的管线逻辑,通过 LayoutNode 驱动 Compose UI 树的变化。同时 Compose Layout 使用 SubcomposeLayout 实现异步测量能力,提高复杂嵌套组件的性能表现。 + +渲染流程对比 +阶段 View System Compose +布局树管理 +View/ViewGroup 层级 +LayoutNode 节点 +渲染方式 +Choreographer + RenderThread +FrameClock + Skia 渲染 +状态追踪 +手动触发 invalidate +Snapshot 自动追踪 + Diff Patch +更新路径 +requestLayout → measure/layout +Recomposer + SlotTable 重组 + +注意:Compose 并非所有场景都一定更快,特别是复杂嵌套、过度组合场景仍需谨慎使用。 + +### 简介 + +Jetpack Compose 是用于构建 Android 界面的新款工具包。 + + +Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建 Android 界面。 + + +编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现 bug 的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。 + +与使用 Android View 系统(按钮、列表或动画)相比,Compose 可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。以下是我们的一些合作伙伴的感想: + +“对于相同的 Button 类,代码的体量要小 10 倍。”(Twitter) +“使用 RecyclerView 构建的任何屏幕(我们的大部分屏幕都使用它构建)的大小也显著减小。”(Monzo) +““只需要很少几行代码就可以在应用中创建列表或动画,这一点令我们非常满意。对于每项功能,我们编写的代码行更少了,这让我们能够将更多精力放在为客户提供价值上。”(Cuvva) +编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易”(Monzo) + +无论您要构建什么,使用 Compose 编写的代码都很简洁且易于维护。“Compose 的布局系统在概念上更简单,因此可以更轻松地推断。查看复杂组件的代码也更轻松。”(Square) + + + +Compose 使用声明性 API,这意味着您只需描述界面,Compose 会负责完成其余工作。这类 API 十分直观 - 易于探索和使用:“我们的主题层更加直观,也更加清晰。我们能够在单个 Kotlin 文件中完成之前需要在多个 XML 文件中完成的任务,这些 XML 文件负责通过多个分层主题叠加层定义和分配属性。”(Twitter) + + +Compose 与您所有的现有代码兼容:您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose,因此您可以随时随地开始采用。“我们集成 Compose 的初衷是实现互操作性,我们发现这件事情已经‘水到渠成’。我们不必考虑浅色模式和深色模式等问题,整个体验无比顺畅。”(Cuvva) + + +为现有应用设置 Compose + +首先,使用 Compose Compiler Gradle 插件配置 Compose 编译器。 + +然后,将以下定义添加到应用的 build.gradle 文件中: +``` +android { + buildFeatures { + compose = true + } +} +``` +在 Android BuildFeatures 代码块内将 compose 标志设置为 true 会在 Android Studio 中启用 Compose 功能。 + + +Compose vs HarmonyOS ArkUI 对比分析 + + +Jetpack Compose 和 HarmonyOS ArkUI 均采用声明式 UI 编程范式,面向多设备场景的响应式 UI 构建,二者在理念相通的同时,在架构设计、状态模型、渲染机制等方面有显著区别。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/compose_vs_arkui.png?raw=true) + + +Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" index 6a4c261c..e3ccc9fc 100644 --- "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" +++ "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" @@ -7,7 +7,10 @@ ConstraintLaayout简介 在`Android Studio`中可以很方便的将目前的布局直接转换成`ConstraintLayout`,可以在布局的编辑页面上直接右键然后选择就可以了。 - +当然项目中要添加它的依赖,目前最新版本: +``` +implementation 'com.android.support.constraint:constraint-layout:1.1.0' +``` 然后会提示添加`ConstraintLayout`支持库。 @@ -90,7 +93,7 @@ ConstraintLaayout简介 * layout_constraintVertical_bias ``` - 下面这段代码就是让左边占30%,右边占70%(默认两边各占50%),这样左边就会短一些,如图5所示,此时代码是这样的: + 下面这段代码就是让左边占30%,右边占70%(默认两边各占50%),这样左边就会短一些,此时代码是这样的: ```xml