7. 插件化和热修复相关相关笔记
- 如何规避Android P对访问私有API的限制?
一种绕过Android P上非SDK接口限制的简单方法:zhuanlan.zhihu.com/p/37819685概念引入:Github:tiann/FreeReflection 《一种绕过Android P对非SDK接口限制的简单方法》 田维术 2018.6.7 知乎链接:https://zhuanlan.zhihu.com/p/37819685 Github链接:https://github.com/tiann/FreeReflection 面试官视角:这道题想考察什么? 1. 是否能够熟练使用Java反射(中级) 2. 是否有Hook调用系统API的开发经验(高级) 3. 是否对底层源码有扎实的语言功底和较强的分析能力(高级) 题目剖析: 1. 私有API包括哪些类型? 2. 如何访问私有API? 3. Android P如何做到对私有API访问的限制? 4. 如何规避这些限制? 题目结论: 一星: 私有API: 1. 被系统隐藏的API,例如: /** * @hide */ @SystemApi public void convertFromTranslucent() {...} 2. private方法 访问私有API: 1. 自行编译系统源码,并导入项目工程(对public hide方法有效) 2. 使用反射 Method initMethod = AssetManager.class.getDeclaredMethod("init"); initMethod.setAccessible(true); 反射不仅可以绕过访问权限的控制,还可以修改 final变量 二星: Android P的 API名单: 白名单:SDK,所有APP均能访问 浅灰名单:仍可以访问的非SDK函数/字段 深灰名单: 对于目标SDK低于API级别28的应用,允许使用深灰名单接口 对于目标SDK为 API 28或更高级别的应用:行为与黑名单相同 黑名单:受限,无论目标SDK如何,平台将表现为似乎接口并不存在。 使用此类成员,都会触发 NoSuchMethodError/NoSuchFieldException 获取此类成员对应的class 的方法和属性列表,亦不包含在内 Android P对反射做了什么? Android P中若反射私有private方法,则Class#getDeclaredMethod(String name, Class<?>... parameterTypes)方法, 对应一个native方法: @FastNative private native Method getDeclaredMethodInternal(String name, Class<?>[] args); 此native方法JNI层对应: static jobject Class_getDeclaredMethodInternal(JNIEnv *env, jobject javaThis, jstring name, jobjectArray args) { ..... if(result == nullptr || ShouldBlockAccessToMember(result->GetArtMethod(), soa.Self())) { return nullptr; } return soa.AddLocalReference<jobject>(result.Get()); } 很明显这里的ShouldBlockAccessToMember(应该阻止成员访问)方法,就是用来判断是否阻止反射访问的: static bool ShouldBlockAccessToMember(T *member, Thread *self) REQUIRES_SHARED(Locks::mutator_lock_) { hiddenapi::Action action = hiddenapi::GetMemberAction(member, self, IsCallerTrusted, hiddenapi::kReflection); ...... return action == hiddenapi::kDeny; } 而在ShouldBlockAccessToMember方法中又能发现若hiddenapi::GetMemberAction方法返回的action == hiddenapi::kDeny,就会block访问。 接下来我们深入看一下hiddenapi::GetMemberAction方法: template<typename T> inline Action GetMemberAction(T* member, Thread* self, std::function<bool(Thread*)> fn_caller_is_trusted, AccessMethod access_method) REQUIRES_SHARED(Locks::mutator_lock_) { DCHECK(member != nullptr); // Decode hidden API access flags. HiddenApiAccessFlags::ApiList api_list = member->GetHiddenApiAccessFlags(); // 第一个Hook点 Action action GetActionFromAccessFlags(member->GetHiddenApiAccessFlags()); if(action == kAllow) return action; // Member is hidden. Invoke fn_caller_in_platform and find the origin of the access. // 第二个Hook点 if(fn_caller_is_trusted(self)) return kAllow; // Member is hidden and caller is not in the platform. // 第三个Hook点 return detail::GetMemberActionImpl(member, api_list, action, access_method); } 三星: 从上面hiddenapi::GetMemberAction方法中有三处Hook点可以绕过私有api访问限制: 1. 第一处hook点: Action action GetActionFromAccessFlags(member->GetHiddenApiAccessFlags()); if(action == kAllow) return action; 优化:修改Runtime 的 hidden_api_policy_ 2. 第二处hook点: if(fn_caller_is_trusted(self)) return kAllow; 优化:将调用者的ClassLoader置空 -Java层直接反射将调用者Class的ClassLoader置为null -Native层直接利用C++对象内存布局直接修改调用者Class的内存地址 3. 第三个hook点: return detail::GetMemberActionImpl(member, api_list, action, access_method); 优化:类似于第一处hook,修改Runtime 的 hidden_api_exemptions FreeReflection使用: 1. 项目中添加依赖项(jcenter): implementation 'me.weishu:free_reflection:2.2.0' 2. Application.attachBaseContext添加一行 : @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Reflection.unseal(base); }
- 如何实现换肤功能?
概念引入:Android 资源加载机制 Android提供了一种非常灵活的资源系统,可以根据不同的条件提供可替代资源。因此,系统基于很少的改造就能支持新特性,比如Android N中的分屏模式。这也是Android强大部分之一 定义资源: Android使用XML文件描述各种资源,包括字符串、颜色、尺寸、主题、布局、甚至是图片(selector,layer-list)。 资源可分为两部分,一部分是属性,另一部分是值。对于android:text="hello,world",text就是属性,hello,world就是值。 属性的定义: 在APK程序中,属性定义在res/values/attrs.xml中,在系统中属性位于framework/base/core/res/res/values/attrs.xml文件中。 <declare-styleable name="Window"> <attr name="windowBackground" format="reference"/> <attr name="windowContentOverlaly" /> <attr name="windowFrame" /> <attr name="windowTitle" /> </declare-styleable> styleable相当于一个属性集合,其在R.java文件中对应一个int[]数组,aapt为styleable中的每个attr(属性)分配一个id值,int[]中的每个id对应着styleable中的每一个attr。 对于<declare-styleable name="Window">,Window相当于属性集合的名称。 对于<attr name="windowBackground">,windowBackground相当于属性的名称;属性名称在应用程序范围内必须唯一,既无论定义几个资源文件,无论定义几个styleable,windowBackground必须唯一。 在Java代码中,变量在一个作用域内只能声明一次,但可以多次使用。attr也是一样,只能声明一次,但可以多处引用。如上代码所示,在Window中声明了一个名为windowBackground的attr,在Window中引用了一个名为windowTitle的attr。 如果一个attr后面仅仅有一个name,那么这就是引用;如果不光有name还有format那就是声明。windowBackground是属性的声明,其不能在其他styleable中再次声明;windowTitle则是属性的引用,其声明是在别的styleable中。 值的定义: 常见的值一般有以下几种: String,Color,boolean,int类型:在res/values/xxx.xml文件中指定 Drawable类型:在res/drawable/xxx中指定 layout(布局):在res/layout/xxx.xml中指定 style(样式):在res/values/xxx.xml中指定 值的类型大致分为两类,一类是基本类型,一类是引用类型;对于int,boolean等类型在声明属性时使用如下方式: <attr name="width" format="integer"/> <attr name="text" format="string" /> <attr name="centerInParent"="boolean"/> 对于Drawable,layout等类型在声明属性时: <attr name="background" format="reference"/> 解析资源:资源解析主要涉及到两个类,一个是AttributeSet,另一个是TypedArray。 AttributeSet: 该类位于android.util.AttributeSet,纯粹是一个辅助类,当从XML文件解析时会返回AttributeSet对象,该对象包含了解析元素的所有属性及属性值。并且在解析的属性名称与attrs.xml中定义的属性名称之间建立联系。AttributeSet还提供了一组API接口从而可以方便的根据attrs.xml中已有的名称获取相应的值。 如果使用一般的XML解析工具,则可以通过类似getElementById()等方法获取属性的名称和属性值,然而这样并没有在获取的属性名称与attrs.xml定义的属性名称之间建立联系。 Attribute对象一般作为View的构造函数的参数传递过来,例如: publlic TextView(Context context,AttributeSet attrs,int defStyle) TypedArray: 程序员在开发应用程序时,在XML文件中引用某个变量通常是android:background="@drawable/background",该引用对应的元素一般为某个View/ViewGroup,而View/ViewGroup的构造函数中会通过obatinStyledAttributes方法返回一个TypedArray对象,然后再调用对象中的getDrawable()方法获取背景图片。 TypedArray是对AttributeSet数据类的某种抽象。对于andorid:layout_width="@dimen/width",如果使用AttributeSet的方法,仅仅能获取"@dimen/width"字符串。而实际上该字符串对应了一个dimen类型的数据。TypedArray可以将某个AttributeSet作为参数构造TypedArray对象,并提供更方便的方法直接获取该dimen的值。 TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0); 加载资源: 在使用资源时首先要把资源加载到内存。Resources的作用主要就是加载资源,应用程序需要的所有资源(包括系统资源)都是通过此对象获取。一般情况下每个应用都会仅有一个Resources对象。 要访问资源首先要获取Resources对象。获取Resources对象有两种方法,一种是通过Context,一种是通过PackageManager。 Resources: 创建Resources需要一个AssetManager对象。在开发应用程序时,使用Resources.getAssets()获取的就是这里创建的AssetManager对象。AssetManager其实并不只是访问res/assets目录下的资源,而是可以访问res目录下的所有资源。 AssetManager在初始化的时候会被赋予两个路径,一个是应用程序资源路径 /data/app/xxx.apk,一个是Framework资源路径/system/framework/framework-res.apk(系统资源会被打包到此apk中)。所以应用程序使用本地Resources既可访问应用程序资源,又可访问系统资源。 AssetManager中很多获取资源的关键方法都是native实现,当使用getXXX(int id)访问资源时,如果id小于0x1000 0000时表示访问系统资源,如果id都大于0x7000 0000则表示应用资源。aapt在对系统资源进行编译时,所有资源id都被编译为小于0x1000 0000。 使用PackageManager获取Resources: PackageManager pm = mContext.getPackageManager(); pm.getResourcesForApplication("com.android...your package name"); 加载应用程序资源: 应用程序打包的最终文件是xxx.apk。APK本身是一个zip文件,可以使用压缩工具解压。系统在安装应用程序时首先解压,并将其中的文件放到指定目录。其中有一个文件名为resources.arsc,APK所有的资源均在其中定义。 resources.arsc是一种二进制格式的文件。aapt在对资源文件进行编译时,会为每一个资源分配唯一的id值,程序在执行时会根据这些id值读取特定的资源,而resources.arsc文件正是包含了所有id值得一个数据集合。在该文件中,如果某个id对应的资源是String或者数值(包括int,long等),那么该文件会直接包含相应的值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的路径地址。 事实上,当程序运行时,所需要的资源都要从原始文件中读取(APK在安装时都会被系统拷贝到/data/app目录下)。加载资源时,首先加载resources.arsc,然后根据id值找到指定的资源。 加载Framework资源: 系统资源是在zygote进程启动时被加载的,并且只有当加载了系统资源后才开始启动其他应用进程,从而实现其他应用进程共享系统资源的目标。 面试官视角:这道题想考察什么? 1. 是否了解Android的资源加载流程(高级) 2. 是否对各种换肤方案有深入的研究和分析(高级) 3. 可以借机引入插件化、热修复相关的话题(高级) 题目剖析: 1. 主题切换 2. 资源加载 3. 热加载还是冷加载 4. 支持哪些类型的资源 5. 是否支持增量加载 题目结论: 系统的换肤支持--Theme 只支持替换主题中配置的属性值 资源中需要主动引用这些属性 无法实现主题外部加载、动态下载 资源加载流程: Context -> Resources -> AssetManager getDrawable -> getDrawable -> openXmlBlockAsset/openNonAsset getColor -> getColor -> getResourceValue getString -> getText -> getResourceText obtainStyledAttributes -> obtainStyledAttributes -> applyStyle openAsset(用来打开Asset目录) 资源缓存替换流:系统Resources里获取资源时,会先从缓存里获取,获取不到则去AssetManager里找。所以可以通过反射将要加载的资源添加进缓存。这样的过程很明显受限于系统缓存的内容,而且由于不同的版本字段会有所变化,适配过程繁琐。 Context Resources getDrawable -> getDrawable -> sPreloadedDrawables/sPreloadedColorDrawables(资源缓存成员,不同版本SDK可能名称不一致) getColor -> getColor -> sPreloadedComplexColors ↓ ↓ (正常逻辑)若资源缓存为null,则调用AssetManger获取 一些流派,虽然资源缓存可能为null,但是通过Skin Resources将资源缓存填充成非null ↓ ↓ AssetManager <-若Skin为null,继续加载 Skin Resources Resources包装流:包装Resources,当然是对其大部分方法都要包装。优先去加载我们添加进去的资源。没有找到,则去走系统的流程。是一个麻烦的过程。 Context -> ResourcesWrapper -> Resources getDrawable -> getDrawable -> getDrawable getColor -> getColor -> getColor getString -> getText -> getText ↓ ↓ Skin Resources Main Resources ResourcesWrapper是我们自己创建的基于Resources的包装类,需要对其大部分方法都要包装。 AssetManager替换流:这是一种系统的加载方案,总的来说,支持的文件种类最多。 该方案使用反射拿到AssetManager里的addAssetPath方法,添加apk路径。这样也会有很多坑,对不同的版本需要进行适配。在创建完activity,也需要将处理好的mResources设置进Context中。 资源替换方案对比:见本体末尾 资源重定向问题:findViewById(R.id.button) 0x7f0500a 主包Table 皮肤包Table R.id.button->0x7f0500a R.id.button->0x7f030004 R.layout.main R.layout.main findViewById中资源id和主包中的一致,而重新打出来的皮肤包对应的资源id已经变啦,因此AssetManager无法根据资源id找到皮肤包中对应的资源。 资源重定向问题解决方案: 动态映射方案:根据资源ID找到名称,然后根据名称和包名找到对应的资源ID findViewById(R.id.button) --> value(资源id) --> name(资源name:id/button) --> value(对应包下的资源id) 静态编译方案: 1. AAPT 编译资源时输入主包的资源id映射,public.xml 2. 编译后根据主包映射关系修改皮肤包的resources.arsc 静态编译方案的问题(资源增量静态对齐): 1. 资源增量静态对齐。资源查找过程中会有一个资源个数判断,所以需要让皮肤包的资源至少大于等于主包的资源数量,不足的部分使用占位资源。 场景:皮肤包中只包含修改后的资源,运行时如果皮肤包中不存在则期望读取主包资源的情况 解决:AAPT编译资源对比主包资源表,皮肤包不存在的资源用空值占位;编译后根据主包映射关系修改皮肤包的resources.arsc用空值占位 2. 资源删除后如果不用占位资源,由于资源编排紧凑的这种方式,其他资源会排到删除的资源位置,这样查找的资源就会出错。这里也需要使用占位资源。 场景:R.attr.attr1皮肤包中未定义编译时AAPT会报错,若剔除public.xml 中的R.attr.attr1,编译时后续非public的资源会由于顺序问题直接占坑。 解决:定制修改AAPT或资源表强制为R.attr.attr1占坑。 皮肤包资源增量差分方案: 编译阶段:主包 + 皮肤包 -(差分)-> 皮肤包差分 应用阶段:主包 + 皮肤包差分 -(合成)-> 皮肤包(直接替换掉主包) 方案: 1. 替换新的AssetManager时只需要添加一个AssetPath 2. App侧有一些合成开销,如果资源包较大,会比较耗时 各类插件化框架也有类似的资源加载诉求,但细节上不同: 1. 换肤框架要保证资源id不变,是覆盖关系。 2. 插件化框架资源id不同,是并存关系。 3. 插件化框架宿主资源共享不存在覆盖。
- Android 资源加载机制详解:www.jianshu.com/p/1d0bfbdaa…
- Android-skin-support:github.com/ximsfei/And…
- VirtualApk如何实现插件化?
概念引入1:Android中的类加载器 Java中的ClassLoader是加载class文件,而Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。 Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是: BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。 PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。 DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。 PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。 PathClassLoader用来操作本地文件系统中的文件和目录的集合。并不会加载来源于网络中的类。Android采用这个类加载器一般是用于加载系统类和它自己的应用类。这个应用类放置在data/data/包名下。 DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。 Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虚拟机加载系统类需要用到的,PathClassLoader是App加载自身dex文件中的类用到的,DexClassLoader可以加载直接或间接包含dex文件的文件,如APK等。 PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,它的一个DexPathList类型的成员变量pathList很重要。DexPathList中有一个Element类型的数组dexElements,这个数组中存放了包含dex文件(对应的是DexFile)的元素。BaseDexClassLoader加载一个类,最后调用的是DexFile的defineClassNative()方法进行加载的。 "*** BaseDexClassLoader:当我们需要加载一个class时,实际是从pathList(DexPathList)中去findClass的,而DexPathList#findClass中会遍历一个装有dex文件(每个dex文件实际上是一个DexFile对象)的数组dexElements(Element数组,Element是一个内部类),然后依次按顺序去加载所需要的class文件,直到找到为止。 即我们在dexElements数组的头部插入一个customdex文件,则ClassLoader查找类时会先查找customdex文件,若有返回若无继续查找其它dex。因此可以使用此种方法,动态下发dex文件来覆盖主App中的同名类(包名和类型均相同),这就是热更新技术原理。" DexPathList源码地址:https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java 概念引入2:Android系统里资源加载查找 Resources对象的生成从下向上一直可以追溯到ContextImpl 的构造方法中: Resources resources = packageInfo.getResources(mainThread); ContextImpl的初始化:ActivityThread#performLaunchActivity-->Application初始化(r.packageInfo.makeApplication,r.packageInfo实际为LoadedApk) --> ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); packageInfo(LoadedApk)中有一成员变量mResDir(构造方法中初始化): // 这个sourceDir,这个是我们宿主的APK包在手机中的路径,宿主的资源通过此地址加载。 mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir; LoadedApk#getResources(mainThread)--> mainThread.getTopLevelResources --> AssetManager assets = new AssetManager(); // 此处将上面的mResDir,也就是宿主的APK在手机中的路径当做资源包添加到AssetManager里,则Resources对象可以通过AssetManager查找资源,此处见(老罗博客:Android应用程序资源的查找过程分析) if (assets.addAssetPath(resDir) == 0) { return null; } // 创建Resources对象,此处依赖AssetManager类来实现资源查找功能。 r = new Resources(assets, metrics, getConfiguration(), compInfo); 从此Android系统可以根据Resources查找资源。 面试官视角:这道题想考察什么? 1. 是否清楚插件化框架如何实现插件Apk的类加载(高级) 2. 是否清楚插件化框架如何实现插件Apk的资源加载(高级) 3. 是否清楚插件化框架如何实现对四大组件的支持(高级) 题目剖析: 1. 不一定讲VirtualApk,说你熟悉的 2. 如何处理类加载 3. 如何处理资源加载和冲突 4. 如何对四大组件进行支持 题目结论: 一星:如何加载运行插件代码? 流程:插件APK --> LoadedPlugin --> 宿主APK,加载插件代码在LoadedPlugin中完成。 LoadedPlugin:若加载插件代码,则需要创建ClassLoader。 protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception { File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR); String dexOutputPath = dexOutputDir.getAbsolutePath(); // loader的父ClassLoader就是宿主的ClassLoader:parent // parent:PluginManager中初始化LoadedPlugin时传入的this.mContext的ClassLoader:context.getClassLoader() // 而this.mContext为 初始化VirtualAPK(PluginManager.getInstance(base).init();)时,通过传入的base(mApplication = (Application)Context)获取的mApplication.getBaseContext()。 DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent); // 若此处为true,默认为true if (Constants.COMBINE_CLASSLOADER) { // 将宿主dexElements插入到插件dexElements前面,因为ClassLoader的双亲委派机制,会按照dexElements数组顺序依次查找加载类。 如果在前面的dexElement里成功加载了一个类, 就不会尝试去后面的dexElement里查找了。 // PS: 宿主和插件若有同一个类(包名、类名相同), 如果COMBINE_CLASSLOADER为true则插件会加载宿主中的类;如果值为false则会加载插件中的类。 DexUtil.insertDex(loader, parent, libsDir); // 此操作后宿主的ClassLoader不仅能加载宿主的类也能加载插件中的类。 } // 若为false,则不插入到宿主中,因此会隔离 // 由于loader的父ClassLoader就是宿主的ClassLoader,因此Constants.COMBINE_CLASSLOADER是否为true,插件APK都可以通过反射访问(共享)宿主类。 return loader; } 流程与ClassLoader对应关系: 插件APK --> LoadedPlugin --> 宿主APK ↓ ↓ DexClassLoader-> PathClassLoader-> BootClassLoader(加载系统类) 对比DroidPlugin超强隔离: new PluginClassLoader(apk, optimizedDirectory, libraryPath, hostContext.getClassLoader().getParent()) 即DroidPlugin的PluginClassLoader父ClassLoader事BootClassLoader,所以PluginClassLoader和PathClassLoader同级,因此宿主和插件、插件和插件之间都无法访问相互的类。 二星:如何处理插件资源? 在VirtualAPK里插件所有相关的内容都被封装到LoadedPlugin里,插件的加载行为一般都在这个类的构造方法的实现里: // 需要注意context是宿主的Context,apk 指的是插件的路径 this.mResources = createResources(context, apk); this.mAssets = this.mResources.getAssets(); protected Resources createResources(Context context, String packageName, File apk) throws Exception { // 是否是组合资源,默认为true if (Constants.COMBINE_RESOURCES) { // 如果插件资源合并到宿主里面去的情况,插件可以访问宿主的资源 // 需要将宿主的APK和插件的APK一起添加到同一个AssetManager里 return ResourcesManager.createResources(context, packageName, apk); } else { // 插件使用独立的Resources,不与宿主有关系,无法访问到宿主的资源 Resources hostResources = context.getResources(); // 这里参照系统的方式生成AssetManager,并通过反射将插件的apk路径添加到AssetManager里 // 这里只适用于资源独立的情况,如果需要调用宿主资源,则需要插入到宿主的AssetManager里 AssetManager assetManager = createAssetManager(context, apk); return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } } Combined Res组合资源编译处理及过滤: 1. 插件和宿主资源没有重复(编译过滤,皮肤包和主包是覆盖) 2. 插件资源id的packageId(即插件工程gralde脚本virtualApk->packageId)被修改(也就是宿主包资源id开头两位,默认必须为0x7f),通过Gradle插件定制资源表,将资源id开头修改为0x6f或其它 过滤方案: 根据资源name和id,若name相同id也相同不处理,若name相同id不同则修改id(同时也需要修改R文件中) 示例: 宿主APK 插件APK int anim abc_fade_in 0x7f010000 int anim abc_fade_in 0x7f010000 ...... int id always 0x7f070012 int id always 0x7f070011 -修改为-> 0x7f070012 int id beginning 0x7f070013 int id beginning 0x7f070012 -修改为-> 0x7f070013 int id button 0x7f070014 int id button1 0x7f070013 -修改为-> 0x7f070014 -由于不是重复的需要修改开头为-> 0x6f070014(此时button1需要往上移动) 注:插件中always和beginning是因为和宿主中name一致而id不一致而修改,button1却不是因为button而修改, button1是因为aapt编译时资源表有资源时,必须是按照顺序往下一个一个顺延下去的,除非前面有空隙(比如always将011改为012,则011就空啦,此时可以将button1改为011,若前面没有空隙则需要顺延) 最后由于插件中的button1不和宿主中的重复,因此需要将开头0x7f改为0x6f或其它(根据插件工程gralde脚本virtualApk->packageId修改) 资源过滤存在的问题: 1. Host Version 1.0 <--> Plugin Version 1.0 若此时Host升级 Version 2.0,此时宿主资源id已改变,导致宿主资源id和插件id不一致,因此插件中加载资源时会出现无法加载插件资源的问题 解决:将R文件资源id的final去掉,这样aapt编译时就不会直接将资源id直接替换成常量值。比如:原来:findViewById(0x7f070012),去final后:findViewById(R.id.always)。此方法只适用于Java代码中,不适用xml中,xml中可以尝试hook Reusorce然后映射一下。 2. 第三方依赖一般不变的资源和插件中的,一样的话删除是没问题的。若是项目自定义和插件中自定义一致时,会出问题,因为两个含义可能不一致。即资源名称相同,资源本身不同 三星: 1. 如何支持启动插件Activity? 欺上瞒下hook:免注册跳转Activity,使用StubActivity代理插件Activity 启动插件Activity的问题: 1. 若Constants.COMBINE_CLASSLOADER不为true,由于启动插件Activity时使用的是宿主ClassLoader,则启动插件Activity时会抛异常。 2. 同1,若宿主Activity启动插件Activity时传入了一个Serializable子类对象时,不将插件ClassLoader注入到宿主ClassLoader时有反序列化问题。因为插件的ClassLoader加载宿主类是加载不到而会抛异常的。 Intent在getExtra时会将里面所有的Extra都解出来,因此这个反序列化问题,会出现在PluginUtil#isIntentFromPligin处。 DroidPlugin如何处理此问题? DroidPlugin会将插件Intent当作Extra放进一个新的Intent中,这样就不会在传递过程中因为getExtra而出问题。这样作的主要原因是因为Intent是Parcelable 2. 如何支持启动插件 Service? 类似于Anctivity:使用LocalService代理插件Service,用RmoteService代理其它进程的插件Service 3. 如何支持注册插件广播? 1. 解析插件Manifest,静态广播转为动态广播 2. 插件广播在宿主未运行时无法被外部唤醒(无法作保活) 3. 系统限制只能静态的广播可在宿主预埋并代理(hook)
- 类加载机制系列2——深入理解Android中的类加载器:www.jianshu.com/p/719360002…
- [VirtualAPK 资源篇]:www.notion.so/VirtualAPK-…
- Tinker如何实现热修复?
面试官视角:这道题想考察什么? 1. 是否有过热修复的实战经验(中级) 2. 是否清楚热修复方案如何对代码进行更新(高级) 3. 是否清楚热修复方案如何对资源进行更新(高级) 4. 是否具备框架设计开发的技术功底和技术素养(高级) 题目剖析: 1. 不一定讲Tinker,说你熟悉的 2. 如何支持代码的热修复 3. 如何支持资源的热修复 题目结论: 一星: Tinker工作流程: Server:Base.apk + New.apk -对比差分-> patch.zip Client:Base.apk + patch.zip -合并-> New.apk 热修复类加载原理:Android中的类加载器PathClassLoader是App加载自身dex文件中的类用到的。PathClassLoader继承自BaseDexClassLoader,它的一个DexPathList类型的成员变量pathList很重要。DexPathList中有一个Element类型的数组dexElements,这个数组中存放了包含dex文件(对应的是DexFile)的元素。 当我们需要加载一个class时,实际是从pathList(DexPathList)中去findClass的,而DexPathList#findClass中会遍历一个装有dex文件(每个dex文件实际上是一个DexFile对象)的数组dexElements(Element数组,Element是一个内部类),然后依次按顺序去加载所需要的class文件,直到找到为止。 即我们在dexElements数组的头部插入一个customdex文件,则ClassLoader查找类时会先查找customdex文件,若有返回若无继续查找其它dex。因此可以使用此种方法,动态下发dex文件来覆盖主App中的同名类(包名和类型均相同),这就是热更新技术类加载原理。 热修复资源加载原理:将AssetManager替换掉。 二星: Java代码热修复-基于Dex的差分算法 首先我们需要将新旧内容排序,这需要针对排序的数组进行操作 新旧两个指针,在内容一样的时候 old、new 指针同时加1,在 old 内容小于 new 内容(注:这里所说的内容比较是单纯的内容比较比如'A'<'a')的时候 old 指针加1 标记当前 old 项为删除 在 old 内容大于 new 内容 new 指针加1, 标记当前 new 项为新增 进入下一步过程 可以确定的是删除的内容肯定是从 old 中的 index 进行删除的 添加的内容肯定是从 new 中的 index 中来的,按照这个逻辑我们可以整理如下内容。 到这一步我们需要找出替换的内容,很明显替换的内容就是从 old 中 del 的并且在 new 中 add 的并且 index 相同的i tem,所以这就简单了 ok,到这一步我们就能判定出两个dex的变化了。很机智的算法 资源热修复-基于Entry的BSDiff 资源插件化主流的做法就是反射调用AssetManager的addAssetPath方法,将插件apk的路径当作参数传进去,然后通过这个AssetManager去创建一个Resource对象就可以了。 资源相关文件差量算法一般流程(即assets目录,res目录,arsc文件,AndroidManifest.xml文件),相关算法如下: 1. 对比new.apk和old.apk中的所有资源相关的文件。 2. 对于新增资源文件,则直接压入patch.apk中。 3. 对于删除的资源文件,则不处理到patch.apk中。 4. 对于改变的资源文件,如果是assets或者res目录中的资源,则直接压缩到patch.apk中,如果是arsc(Resources.arsc,资源索引表)文件,则使用bsdiff算法计算其差量文件,压入patch.apk,文件名不变。 5. 对于改变和新增的文件,通过一个res_meta.txt文件去记录其原始文件的Adler32(校验和 算法)和合成后预期文件的Adler32值,以及文件名,这是个文本文件,直接压缩到patch.apk中去。 Tinker资源补丁生成: ResDiffDecoder.patch(File oldFile, File newFile)主要负责资源文件补丁的生成,如果是新增的资源,直接将资源文件拷贝到目标目录;如果是修改的资源文件则使用dealWithModeFile函数处理。 在dealWithModeFile中会对文件大小进行判断,如果大于设定值(默认100Kb),采用bsdiff算法对新旧文件比较生成补丁包,从而降低补丁包的大小;如果小于设定值,则直接将该文件加入修改列表,并直接将该文件拷贝到目标目录。 接着ResDiffDecoder.onAllPatchesEnd()中会加入一个测试用的资源文件,放在assets目录下,用于在加载补丁时判断其是否加在成功。这一步同时会向res_meta.txt文件中写入资源更改的信息。 三星: 细致的异常处理:loadTinkerResources方法 核心代码:TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceSting); 除此一句核心代码外,均是日志、校验MD5、计算统计耗时、异常处理、若失败则卸载热修复包 监控验证&闭环意识:V3.0-异常熔断、监控回调
8. 追求极致优化相关笔记
- 如何开展优化类工作
面试官视角:这类题想考察什么? 1. 是否对项目整体目标有清晰认识(高级) 2. 是否能对项目的重点问题进行拆解(高级) 3. 是否有追求极致的技术和主观意愿(技术强,但是面对问题得过且过也不好)(高级) 4. 是否能够在关键时刻承担有挑战的工作(高级) 题目剖析: 1. 通常作为大项目的重点专项存在 2. 对整个项目具有系统性、全局性的认识,此类工作一般属于系统性全局性的局部工作 3. 可以凸显追求极致的精神 题目结论:答题模板(按照模板的思路按需(自己的项目经历)答题) 明确优化目标: 优化方向: 耗电量优化? 过度绘制优化? 内存优化? CPU占用率优化? 算法策略优化? 优化目标: 定性(咱们App耗电量太高,需要优化!)————>定量(后台运行 10%/小时,目标 3%/小时) 注:体现你能确立清晰的优化目标,而不是想当然的开展优化 定位关键问题:优化占比最高的问题 二八定律:80% 的错误通常源自于 20%的问题 -优化前期花 20%的精力就能解决 80%的问题 -优化后期则相反 业内横向对比:体现你考虑问题比较全面,而不是盲目造轮子(若生态中已有现成解决方案,只是方案有些许地方不适合,则可通过优化修改现有方案,而不是单纯的造轮子) 完善指标监控:对应用的性能、业务可靠性进行线上的监控和预警 灰度上线:按产品需求优先级,抽出核心需求,在满足用户基本要求的情况下快速上线,并通过限制流量、白名单等机制进行产品试用,以此收集用户的意见,从而萃取出用户潜在的需求,形成后续更有针对性的设计方案。 和传统研发模式相比,这么做唯一的区别就在于将原先一锅粥式的需求和功能点进行了轻重缓急的排序,并以此将项目从原来的单长线作战转化为多迭代短线循环,让产品的生命周期不再昙花一现。 如此一来,需求分析阶段显得尤为关键,我们必须清晰的将需求按优先级归纳分类为几个序列,如:p1,p2,p3…核心功能和必备的体验在p1序列,辅助功能点和辅助型体验列在p2序列,争执不定的需求点可以放在p3序列。 需求排序后,我们可以将项目发布点有序的分成(>2期),第一期只确保主要的核心功能和基础体验快速灰度上线,随后通过用户访谈、产品的tracker&session数据、业务数据等手段分析出用户对产品的真实反应,并以此调整二期需求,该加的加,该砍的砍,做到有的放矢。 项目收益:转换成面试官有概念的指标 -页面加载时间减少 800ms -内存消耗降低 50MB -CPU占有率由 12%降低至 3% -项目成本降低xxx 人力优化: 优化目标: 1. 主要针对项目负责人 2. 需要对项目成员的能力有足够的了解 3. 需要对项目功能做合理的拆解 4. 用合适的人做合适的事 5. 适当放权,但也要依据情况做好指导 优化心法: 1. 深入钻研技术为优化提供可能 2. 结合业务场景为优化提供落脚点 3. 熟悉团队特点为优化提供战斗力 不好的例子: 简历:项目当中用到一个 xxx 算法来加密,加密过程中比较耗时,为了优化性能,我采用并发的方式来提升算法的速度。 面试官提问: Q1:你的程序是IO密集型还是CPU密集型 答:CPU密集型 Q2:为什么CPU密集型的程序可以通过并发提升效率? 答:因为...耗时主要是因为算法结果需要插入数据库,由于数据量比较大,IO耗时比较长,应该是IO密集型的。 答解析:CPU密集型程序,证明CPU基本一刻也不会闲着,而现在开多个线程只会增大CPU的压力,因为线程之间的切换是有开销的。 Q3:再想想,是加密过程耗时还是入库耗时 答:入库耗时,这里写错啦.. Q4:并发一定能提升运行速度吗?你的线程开了多少个? 答:能吧,开了三个。 Q5:如果设备是单核CPU呢? 答:额..没想过 答解析:多线程的用处在于,做某个耗时的操作时,需要等待返回结果,这时用多线程可以提高程序并发程度。如果一个不需要等待的CPU密集的任务,效率提升不明显.但是如果是IO操作比较多,CPU计算比较少,即使是单核CPU,应用了多线程技术后对外也会表现一定性能提升(实质为提高了碎片时间的利用率) 不好的例子--技术优化: 简历:项目当中用到一个 xxx 算法来加密,加密过程中比较耗时,为了优化性能,优化了算法,耗时减少 40% 面试官提问: Q1:你是怎么优化这个算法的? 答:1. 这个算法本身涉及到大量的矩阵运算,在Java层运行我发现会导致频繁gc,因此我在算法运行时做了一些小矩阵的池化,减少对象的频繁创建和gc,耗时减少 20% 2. 后来考虑到算法的可移植性,我用 c 重写了,发现单次计算耗时比Java版本又减少 20%,一是内存直接使用物理内存,减少gc,二是算法执行过程中需要频繁与底层函数交互,c版本将原来jni调用变成了直接调用,降低了开销。由于我们这个算法的调用频次不高,因此c版本接口层引入的jni调用开销可以忽略不计。 不好的例子--业务优化: 简历:项目当中用到一个 xxx 算法来加密,加密结果数据较大,入库比较耗时。我对整个业务流程进行了优化,减少IO的耗时,同时也在合适的情况下开启多线程操作IO,提升运行效率。 面试官提问: Q1:你是怎么优化这个业务流程的? 答:1. 加密结果较大是源数据较大导致的,源数据由前序业务生产环节产生。我与相关同时经过探讨,在不影响业务的前提下优化了数据格式,将JSON格式的源数据修改为Protobuf进行加密,源数据减少了 60%。 2. 源数据存储于sqlite,实验发现其二进制读写性能不如文件直接读写,因此同样不影响源数据的情况下直接从文件系统读取,性能提升 5%。 3. 将算法做了优化,确保安全的前提下,由原先的全文件加密改为局部加密,文件不需要完整加载和回写,直接随机读写文件系统就可以解决,IO耗时减少 90%。 本节回顾: 1. 探讨了优化类项目开展和阐述的关键思路 -量化指标,完成从定性到定量的转变 -定位问题,二八定律与关键问题的解决 -横向对比,避免遭到为什么自己造轮子的挑战 -完善监控,优化效果有据可查 -项目收益,给出听得懂的收益指标 -人力优化,合适的活给合适的人 2. 结合一个不好的例子并给出改进方案
- 一个算法策略优化Case:
优化方向: -结果确定性:主要是性能优化 -结果模糊性:受样本影响大,算法本身的优化 算法模糊优化: 优化前的项目状况 -指标:xxx准确率,例如:语音识别准确率 -量化方案:无,主要凭感觉 -策略验证方案:无 优化: 量化指标: -给出xxx准确率的数学定义和计算方法 -建立指标获取、策略验证的流程和方法 -搜集建立充足的样本集得到指标的现状 -确定合理的KPI,例如从 78% 优化到 92% 对比现有技术方案: -阅读相关学术论文 -与相关经验的团队进行技术交流 监控体系建设: -针对算法效果的指标 xxx准确率做监控 -根据项目特征确定指标汇报频率 -定期发送线上运营数据报表,展现项目效果 算法策略动态下发: 算法迭代--> 动态下发(插件化或者脚本化)--> 算法生效 工具完善:人工-->自动指标量化-->辅助问题定位 灰度上线: -初期:线上数据离线核验,项目核心用户灰度 -中期:线上数据离线核验,线上用户30%灰度 -后期:逐步全量推送 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing, 即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围, 把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 本节回顾: -指标量化:确定计算方法、制定目标 -问题定位:分析问题、方案对比 -问题解决:解决 80% 的问题、追求极致 -项目收益:直接收益,宏观收益
- 一个工程技术的优化Case
项目背景:一个视频截图SDK的效率优化工作(输入视频和时间戳,截图) 视频格式: 编码格式 封装格式:举例 MP4:索引 + 视频数据 Mpeg2-Ts文件:... + 同步信息 + 视频数据 + 同步信息 + 视频数据 + ... 帧:I-关键帧/P-依赖于前面关键帧/B-不仅依赖于前面关键帧,还依赖后面帧 优化前的项目状况: 量化指标:单张图像截取耗时 验证策略:选择大量样本随机截帧计算耗时 对比现有技术方案: MediaMetadataRetriever:Android系统 API FFmpegMediaMetadataRetriever:基于FFmpeg封装,接口与系统对齐 基于FFmpeg自研:基于FFmpeg,可按需求定制 方案类型 系统原生 FFmpeg 支持格式 较少 常见所有格式 兼容性 受线于硬件和系统,较差 基本没有兼容性问题 MP4 约1.5s 约1.1s(目标帧只占很小比例) TS 1.3s,有偏差且无法获取 3s,无偏差(大量时间用于寻址) 内部逻辑 不可控,不可定制 可控,可定制 解码器选择:优先硬件解码,可根据设备情况动态切换 方案类型 硬件解码 软件解码 支持类型 较少 常见所有格式 兼容性 受限于硬件,较差 非常好,已于扩展 解码速度 非常快 相对较慢 开源框架License 优化方案: 1. 纯技术优化:TS快速寻址 2. 结合业务的优化点: -I帧近似 -编解码流程优化
9. 拆解需求设计架构相关笔记
- 如何解答系统设计类问题?
面试官视角:这道题想考察什么? 1. 是否能够快速理解需求并对需求进行拆解(中级) 2. 是否具备广泛的技术栈或知识面(高级) 3. 是否能够深入挖掘需求给出良好的技术方案(高级) 4. 是否具备良好的项目管理和领导能力(高级) 题目剖析: 1. 解答过程中与面试官要保持良好的沟通 2. 如果系统足够大,则不需要解释太多细节 3. 如果系统较小,最好辅以精妙的细节设计 题目结论: 项目诞生: 提出想法-->可行性分析-->需求分析-->系统设计-->系统开发-->迭代维护-->系统重申 面试官:提出想法 候选人:需求分析-->系统设计 系统设计步骤: 需求:设计(项目需求)一个网络框架 流程:关键就是打包请求、建立连接、发送请求、解析结果 细节:请求和响应的数据结构适配能力、请求重试机制、异步处理能力、使用体验优化 回顾:如何用Java实现Handler 需求:一直Android Handler到Java平台 流程:关键消息队列、死循环、阻塞和延时 细节:是否需要支持底层、消息队列性能优化、消息实例池化 系统设计三步走: 1. 明确边界 2. 打通流程 3. 优化细节 细节通用点: 如何处理好并发: -是否有频繁的IO操作? -线程调度如何设计?比如:线程池,那么线程池上限是多少?根据什么限制设置线程池最大的线程数?CPU核心数 -业务操作中异步程序如何设计? * RxJava * 协程(kotlin) 网络如何接入? -是否需要频繁与服务端交互? -是否存在服务端主动推动消息的场景? -采用何种通信手段 * 长连接:高频交互,消息推送,维护复杂 * 短连接:低频交互,伪消息推送(短轮询(每个一段时间访问网络)、长轮询(访问后若无消息返回,则定时较长时间访问,比如:60s或120s等)) 保障安全性: -数据是否需要加密? -加密算法如何选择? * 对称加密:密钥如何保存? * 非对称加密:注意加密复杂度限制 -应用安全性如何保证? * 混淆 * 加固 * 对签名进行验证 热修复与插件化: -热修复一般都需要,关键看方案选型 * 是否要求立即生效?若客户的设备打开头一直运行应用直到断电等,则需要立即生效。正常手机App可重启生效,比如:Tinker。 * 是否要求新增或者修改类? -插件化主要考虑体量 * 前期通常不需要插件化,但可未雨绸缪 * 是否融合了多条业务线,多团队协作? 脚本化: -是否存在大量可模式化的逻辑: * 游戏关卡 * 自定义的UI体系 -是否存在大量需要经常调整的策略? * 简单的参数调整无法满足 可移植性: -是否存在复杂的平台不相关的逻辑? * 如语音识别、OpenGL绘制逻辑 * 考虑C++开发 -是否考虑移植UI? * Flutter/React Native 性能问题: -算法的时间和空间复杂度? -内存峰值是否偏大有无OOM可能? -CPU占有率是否持续较高? -耗电量是否居高不下? 监控: -异常捕获以及状态保存恢复 * Java层异常捕获 * Native层异常捕获 -性能监控 -优化指标监控 -运营数据监控 思考过程: 系统设计题没有标准答案 深思熟虑地选择技术方案 展示你的知识深度和广度 思考过程比最终结果重要 本节回顾: 三个步骤: 明确需求、打通流程、优化细节 十个方面: 并发网络与安全 脚本热修复插件 性能监控可移植 思考过程是重点
- 设计一个短视频App
面试官视角:这道题想考察什么? 1. 是否对短视频乃至视频行业业务有认识(中级) 2. 是否有丰富的系统设计架构经验(高级) 3. 是否对音视频相关技术领域有一定的积累(高级) 题目剖析: 1. 视频处理一定是重点 2. App设计除业务本身外其余大多数相同 题目结论: 一星: 明确需求变界: 视频来源自有服务还是第三方?自有服务可同一格式,若是第三方则可能需要兼容多种格式 视频由用户上传还是专业供应平台提供?视频上传和录制 是否需要建立用户关系链? 是否需要支持视频分享? 是否需要建立支付系统方便打赏? 打通关键流程: 发布者: 录制视频 上传视频 订阅者: 下载视频 播放视频 基础组件:网络框架 播放器 相机 滤镜算法 业务模块:视频 Feed流 社交 钱包 账户 二星: 播放器可移植 公司平台级App 短视频细分App Android IOS Android IOS 播 放 器 滤镜脚本化:Camera--> OpenGL(Shader Script)--> View 安全性: 视频文件安全性,防止竞品非法获取 加密耗时影响体验,注意加密算法选取 应用做好混淆和加固,防止篡改广告 三星: 成本优化 视频编码格式: 指标 H.264 H.265 硬件支持 几乎全部 很少 文件大小 1 0.5 编解码耗时(硬件) 1 3-7 建议: 针对热点视频(访问量大,带宽占比高)采用H.265 针对性能较好的机型动态切换软解H.265与硬解H.264 播放优化 MP4文件 包括:File Type Box(ftpy)、Movie Box(moov,索引)、Media Data Box(mdat,帧数据),若moov中缺失一点,就会导致整个MP4无法播放 格式: 1. ftpy + moov + mdat,这种比较友好,可以边下载边播放 2. ftpy + mdat + moov,需要下载完才能播放,若在录制过程中moov由于异常缺失一点,就会整个文件损坏 格式优化: ftpy + moov + mdat ↑ Server ↑ ftpy + mdat + moov 播放优化2: 播放器 播放行为(开始播放点) ios 一个GOP(推测) Android 6.0及以下 5s视频数据 Android 7.0及以上 一个GOP(gop,意味着一组帧,从第一个关键帧到第二个关键帧之前) 基于FFmpeg自研播放器 关键帧(即一开始就可以播放) 流量合作:专属流量,降低用户使用成本
- 设计一个网络请求框架
面试官视角:这道题想考察什么? 1. 是否具备扎实的网络通信基础(中级) 2. 是否有丰富的网络开发经验及需求细化能力(高级) 3. 是否具备通用基础框架的架构设计能力(高级) 4. 是否有框架使用体验优化的意识和思路(高级) 题目剖析: 1. 所有跟网络有关的,不要局限于Http 2. 框架设计的几点注意事项 依赖关系尽可能简单 对外接口尽可能易用 功能设计尽可能纯粹 题目结论: 一星: 明确需求变界: 单向请求还是双向请求? 需要支持哪些应用层协议? 是否要支持自定义协议扩展? 是否需要支持异步能力? 运行在什么平台上(可移植性)? 打通关键流程: 协议层:Http WebSocket 基础组件:连接管理 线程管理 Tops:关键模式可绘制UML图 Connection: int write(byte[*] bytes, int start, int offset) int read(byte[*] bytes) void close() void recycle() // 复用 ConnectionManager: Connection create(URL remoteUrl, boolean reuse) 二星: 为Http协议添加缓存机制: 淘汰策略:默认采用LRU算法 存储位置:内存、磁盘 接口开发:全局开启或禁用缓存,策略、参数可配置 增加全局数据拦截: Client Interceptor Server 模拟服务:拦截器Interceptor 日志工具:Client 日志输出 Server 重试机制:失败越多,重试间隔越大,并且有上限次数。可设置最大重试次数,可指定频率衰减因子。 三星: 使用注解配置请求:Retrofit 第三方扩展:RxJava、Kotlin 代码设计模式: 协议体构建使用Builder 数据的传输与拦截使用责任链模式 数据序列化类型支持使用适配器模式 主要涉及的高级语法: 注解:主要用于接口配置和参数解析 泛型:主要用于数据类型的适配 反射:读取注解信息、反序列化类型等等 DNS增强:阿里云、腾讯云(HttpDNS,移动解析)
注:该系列文章主要参考于《大厂资深面试官 带你破解Android高级面试》
此篇文章为系列文章中最后一篇,最后一篇文章能力有限,只能尽我可能写完整,其中很多知识点工作中从未涉及,比如:插件化,工作之间没有使用过,第一次尝试使用就是在学习该系列课程中,也遇到了很多坑,最终终于还是构建了一个简单的demo。热修复,本人也是从未使用过,因此该部分内容只能跟着面试课程勉强记录下来。最后,感谢大家对本系列笔记的认可和支持。