组件化 模块化:项目按照独立的模块进行划分
项目组件化的重要环节在于,将项目按照模块来进行拆分,拆分成一个个业务module和其他基础module(lib),各个业务module之间互不依赖,互相解耦!每个业务module都可以安排不同的开发人员团队来进行开发,不强制使用一种开发模式,MVP可以,MVC也可以!然后各个业务module之间通过路由机制进行跳转和传递!
资源名冲突 color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题
1 2 3 4 android { // 所有xml资源命名以 "login_" 开头、否则编译报红 resourcePrefix "login_" }
组件化模块间交互
使用 EventBus 的方式,缺点是:EventBean 维护成本太高,不好去管理:
使用广播的方式,缺点是:不好管理,都统一发出去了
使用隐士意图方式,缺点是:在 AndroidManifest. xml 里面配置 xml 写的太多了
使用类加载方式,缺点就是,容易写错包名类名,缺点较少
使用全局 Map 的方式,缺点是,要注册很多的对象
网上开源的组件化 1、组件数据交互 · luojilab/DDComponentForAndroid Wiki
1 Router.getInstance ().getService (MedRecordDossierService::class.java .name).getPatientFragment
核心思路非常朴素且高效:面向接口编程 + 中央 Router(Service Locator)做实现的注册与查找 + 组件生命周期钩子控制注册/反注册 。 这样组件之间既能互相调用,又不需要互相依赖实现代码,达到彻底解耦。
1. 交互的契约层:把“接口”单独下沉 项目推荐把所有组件对外暴露的服务接口统一放到 componentservice 这个 module 里(只放接口、数据结构等“契约”)。
示例 :例如 reader 组件要对外提供一个能力(返回一个阅读页 Fragment),就在 componentservice 定义:
ReadBookService(接口,只描述能力,不包含实现)
这样其它组件只需要依赖 componentservice,不会依赖 reader 组件本身。
2. 实现层:实现类留在组件内部,外部不可见 reader 组件内部自己实现 ReadBookServiceImpl,并返回真实的 ReaderFragment。 由于组件代码隔离,这个实现类对外部组件不可见,外部永远只能看到接口。
3. 注册与发现:Router 充当中央“服务注册表” Router 内部的数据结构 Router 里维护了一个 HashMap<String, Object> services,用来保存 “serviceName -> serviceImpl” 的映射,并提供以下方法
addService(serviceName, impl)
getService(serviceName)
removeService(serviceName)
所以它本质上就是一个线程安全(synchronized)的 Service Locator 。
注册时机:每个组件的 ApplicationLike 每个组件提供一个 ApplicationLike(相当于“组件自己的 Application”,用于组件生命周期控制)。
加载时 :在组件加载时 onCreate() 里把实现注册进 Router。
卸载时 :在组件卸载时 onStop() 里反注册。
1 2 3 4 router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl ()); router.removeService(ReadBookService.class.getSimpleName());
这就是“组件可动态加载/卸载”时交互仍然可控的关键。
4. 调用方式:使用方从 Router 拿接口再调用 其它组件要使用 reader 的能力时:
调用 Router.getInstance().getService(ReadBookService.class.getSimpleName())
判空 (因为组件可能没被集成/没加载)
强转为接口类型 ReadBookService
调用接口方法拿到结果(例如 Fragment)
关键点 :调用方只依赖接口,不依赖实现,也不需要知道实现类名。
5. 为什么它能做到“组件动态加载/卸载” 除了 services,Router 还维护了一个 components 表(HashMap<String, IApplicationLike>),并提供:
registerComponent(classname):通过 Class.forName(classname) 反射创建 IApplicationLike,调用其 onCreate(),然后记录到 components 中。
对应也有反注册逻辑:卸载时会走 onStop() 并从表中移除。 因此,“组件是否可被调用”完全取决于它是否完成了 ApplicationLike 的注册流程,以及服务表里是否存在对应实现。
6. 这种交互方式的特点与边界
特点
描述
真正解耦
组件间只通过接口交互。
支持按需集成
没集成/没加载的组件,getService() 返回 null,调用方可降级处理。
支持动态卸载
onStop() 反注册即可切断能力暴露。
彻底明白 Phone Module与 AndroidLibrary的区别 Phone Module 可以单独运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 sourceSets { main { if (!isRelease) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { exclude '**/debug/**' } } } }
项目重构
项目由于业务需求越来越大,不同项目的功能想相互迁移变的越来越难,经过查阅大量文章,推动项目组件化,分成基础组件层、业务组件层、业务module层,从下往上依赖,module是不同的业务,如社区、还款等,方便迁移并且可以独立运行。
传递数据是通过scheme,scheme是一种页面内跳转协议,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面:通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转指定页面。
基础组件层: 底层使用的库和封装的一些工具库(libs),比如okhttp,rxjava,rxandroid,glide、相机、照片等
业务组件层: 与业务相关,封装第三方sdk,比如封装后的支付等、自定义view、工具类
业务模块层: 按照业务划分模块,比如说还款模块,社区模块等
1 2 3 4 5 6 7 8 <intent-filter > <action android:name ="android.intent.action.VIEW" /> <category android:name ="android.intent.category.DEFAULT" /> <category android:name ="android.intent.category.BROWSABLE" /> <data android:scheme ="qianbaoguanjia80e866a23d" /> </intent-filter >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("qianbaoguanjia80e8565663d://brushcard?goWhere=" + "1" )); startActivity(intent); Intent intent = getIntent();String action = intent.getAction();if (Intent.ACTION_VIEW.equals (action)) { Uri uri = intent.getData(); if (uri != null ) { String dataString = intent.getDataString(); Log .d("Alex" , "dataString:" + dataString); String host = uri.getHost(); if (host.equals ("brushcard" )) { String goWhere = uri.getQueryParameter("goWhere" ); if (goWhere.equals ("1" )) { return ; } } return ;
插件化
减少体积、添加功能
提高打开速度(多个 dex,效果不理想)
为什么有反射:private 防不住的,不是限制做坏事,是限制不小心写错了
把插件 apk 放在 asset 里,或者网络下载,保存在本地,可以通过 dexClassLoader 加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); File apk = new File (getCacheDir() + "/plugin-debug.apk" ); try (Source source = Okio.source(getAssets().open("plugin-debug.apk" )); BufferedSink sink = Okio.buffer(Okio.sink(apk))) { sink.writeAll(source); } catch (IOException e) { e.printStackTrace(); } DexClassLoader classLoader = new DexClassLoader (apk.getPath(), getCacheDir().getPath(), null , null ); try { Class utilsClass = classLoader.loadClass("com.demo.pluginnable_plugin.Utils" ); Constructor utilsConstructor = utilsClass.getDeclaredConstructors()[0 ]; Object utils = utilsConstructor.newInstance(); Method shoutMethod = utilsClass.getDeclaredMethod("shout" ); shoutMethod.invoke(utils); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } Intent intent = new Intent (); intent.setClassName(this , "com.hencoder.a37_pluginnable_plugin.SecondActivity" ); startActivity(intent); } }
动态加载其他 apk 的 activity,可以设置个代理类,因为一开始是不知道要启动的名字是叫什么的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class ProxyActivity extends AppCompatActivity { Object realActivity; @Override protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); } @Override protected void onStart () { super .onStart(); } @Override public Resources getResources () { return new Resources (getAssets(), super .getResources().getDisplayMetrics(), super .getResources().getConfiguration()); } private AssetManager createAssetManager (String dexPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath" , String.class); addAssetPath.invoke(assetManager, dexPath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null ; } } }
ClassLoader
ClassLoader有PathClassLoader、DexClassLoader俩个子类
Android 8.0 与 DexClassLoader 完全一致 ,这个 optimizedDirectory 在 Android 8.0 以后也被舍弃了,只能使用系统默认的位置了。
5.0–8.0
PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 可以指定 optimizedDirectory ,也就是 dex2oat 的产物 .odex (优化后的 dex 文件)存放的位置,而 PathClassLoader 只能使用系统默认位置。
DEX 文件同级目录下添加一个 oat/ 文件作为 .odex 的存储目录。
android 4.4之前
PathClassLoader 只能加载安装在 Android 系统内 APK 文件(*/data/app 目录下*),其他位置的文件加载时都会报 ClassNotFoundException。因为 PathClassLoader 会读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的,没有安装的时候,自然没有生成这个文件。
对于 App 而言,Apk 文件中有一个 classes. dex 文件,那么这个 dex 就是 Apk 的主 dex, 是通过 PathClassLoader 加载的。
DexClassLoader:可以加载任意目录下的 dex/jar/apk/zip 文件,是实现热修复的重点。
PathClassLoader:用于加载 Android 系统类和开发编写应用的类,只能加载已经安装应用的 dex 或 apk 文件,是 Android 默认使用的类加载器,也是 getSystemClassLoader 的返回对象。
BootClassLoader:主要用于加载系统的类,包括 java 和 android 系统的类库,和 JVM 中不同,BootClassLoader 是 ClassLoader 内部类,是由 Java 实现,它也是所有系统 ClassLoader 的 父 ClassLoader。
我们可以指定到 getCacheDir().getPath()
在 App 的 Activity 中,通过 getClassLoader 获取到的是 PathClassLoader, 它的父类是 BootClassLoader。
loadClass() 的类加载过程 双亲委托机制 宏观上:是⼀个带缓存的、从上到下的加载过程(即网上所说的「双亲委托机制」) 这样做的意义是为了性能,每次加载都会消耗时间,但如果父亲加载过,就可以直接拿来使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null ) { try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { c = findClass(name); } } return c; }
代理 startActivity
热更新 热更更新和插件化的区别
插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
插件化在代码中有固定的入口,而热更新则可能改变任何一个位置的代码,热更新的原理 ClassLoader 的 dex 文件替换,直接修改字节码。
热修复(热更新)主要用来修复代码、修复bug,而插件化是增加新的功能类或者是资源文件。
应用场景
当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,你得:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。
热修复和插件化的原理主要是操作ClassLoader
Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class文件加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader(类加载器)。
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element(元素),多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找不到从下一个dex文件继续查找,如果找到类则返回。
然后就可以做一些事情,比如,在这个数组的第一个元素放置我们的补丁,里面包含修复过的类,这样的话,当遍历findClass的时候,后面的就会忽略 。
热更新的关键在于,把补丁dex文件加载放进一个Element ,并且插入到dexElements这个数组的前面(插入到后面的话会被忽略掉)
手写热更新
创建一个新的dexElements,把补丁的每个DexElement放到新的数组里,把旧的都拷贝到新的后面,然后把数组换了 newClassLoader.pathList.dexElements
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 public class HotfixApplication extends Application { File apk; @Override protected void attachBaseContext (Context base) { super .attachBaseContext(base); apk = new File (getCacheDir() + "/hotfix.dex" ); if (apk.exists()) { try { ClassLoader classLoader = getClassLoader(); Class loaderClass = BaseDexClassLoader.class; Field pathListField = loaderClass.getDeclaredField("pathList" ); pathListField.setAccessible(true ); Object pathListObject = pathListField.get(classLoader); Class pathListClass = pathListObject.getClass(); Field dexElementsField = pathListClass.getDeclaredField("dexElements" ); dexElementsField.setAccessible(true ); Object dexElementsObject = dexElementsField.get(pathListObject); PathClassLoader newClassLoader = new PathClassLoader ( apk.getPath(), null ); Object newPathListObject = pathListField.get(newClassLoader); Object newDexElementsObject = dexElementsField.get(newPathListObject); int oldLength = Array.getLength(dexElementsObject); int newLength = Array.getLength(newDexElementsObject); Object concatDexElementsObject = Array.newInstance( dexElementsObject.getClass().getComponentType(), oldLength + newLength ); for (int i = 0 ; i < newLength; i++) { Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i)); } for (int i = 0 ; i < oldLength; i++) { Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i)); } dexElementsField.set(pathListObject, concatDexElementsObject); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } } } public class MainActivity extends AppCompatActivity { TextView titleTv; Button showTitleBt; Button hotfixBt; Button removeHotfixBt; Button killSelfBt; File apk; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); titleTv = findViewById(R.id.titleTv); showTitleBt = findViewById(R.id.showTitleBt); hotfixBt = findViewById(R.id.hotfixBt); removeHotfixBt = findViewById(R.id.removeHotfixBt); killSelfBt = findViewById(R.id.killSelfBt); apk = new File (getCacheDir() + "/hotfix.dex" ); View.OnClickListener onClickListener = new View .OnClickListener() { @Override public void onClick (final View v) { switch (v.getId()) { case R.id.showTitleBt: Title title = new Title (); titleTv.setText(title.getTitle()); break ; case R.id.hotfixBt: try (Source source = Okio.source(getAssets().open("apk/hotfix.dex" )); BufferedSink sink = Okio.buffer(Okio.sink(apk))) { sink.writeAll(source); } catch (IOException e) { e.printStackTrace(); } break ; case R.id.removeHotfixBt: if (apk.exists()) { apk.delete(); } break ; case R.id.killSelfBt: android.os.Process.killProcess(android.os.Process.myPid()); break ; } } }; showTitleBt.setOnClickListener(onClickListener); hotfixBt.setOnClickListener(onClickListener); removeHotfixBt.setOnClickListener(onClickListener); killSelfBt.setOnClickListener(onClickListener); } } def patchPath = 'com/hencoder/a38_hotfix/Title' task hotfix { doLast { exec { commandLine 'javac' , "./src/main/java/${patchPath}.java" , '-d' , './build/patch' } exec { commandLine 'C:\\Users\\41009\\AppData\\Local\\Android\\Sdk\\build-tools\\29.0.2\\d8.bat' , "./build/patch/${patchPath}.class" , '--output' , './build/patch' } exec { commandLine 'mv' , "./build/patch/classes.dex" , './build/patch/hotfix.dex' } } }
tinker 好文章 有 Git 才有 tinkerID,有还报错的话本地提交一次。 需要改造 Application,不支持 AndroidDaggerhttps://www.jianshu.com/p/63fcffa3d4b2 坑太多,建议使用 http://www.tinkerpatch.com/
tinker 将 old. apk 和 new. apk 做了 diff,拿到 patch. dex,然后将 patch. dex 与本机中 apk 的 classes. dex 做了合并,生成新的 classes. dex,运行时通过反射将合并后的 dex 文件放置在加载的 dexElements 数组的前面。
AndFix 原理: 方法的替换,把有bug的方法替换成补丁文件中的方法。
优点 : 重大bug,需要紧急修复 可以下次迭代修复的bug 影响用户体验的行为 无需重启
缺点 : 无法添加新类(内部类也不行)和新的字段、新的方法 资源文件无法替换 试了下换原有的图片可以,但是新增的不行 不能修改xml布局文件 不能 加固后的包补丁无法使用,如果要加固,需要加固前的包来生成补丁,不过这样生成的补丁也很容易破解 不能对同一个方法修复两次,否则App根本跑不起来 对加载过的补丁文件要做名字修改 如果名字重叠 就不会再次加载
补丁加载的时机 可以放在自定义Application的onCreate方法中,也可以放在button的点击事件中,也可以放在监听网络变化的广播中。 加载过的补丁会被保存到data/packagename/files/apatch_opt目录下,所以下载过来的补丁用过一次就可以删除了。
操作: 通过命令生成补丁
1 2 3 4 5 6 7 8 9 10 -a ,--alias <alias> keystore entry alias. -e,--epassword <\*\*\*> keystore entry password. -f,--from <loc> new Apk file path. -k,--keystore <loc> keystore path. -n,--name <name> patch name. -o,--out <dir> output dir. -p ,--kpassword <\*\*\*> keystore password. -t,--to <loc> old Apk file path. apkpatch -f D:\\addfixUtils\\2 .apk -t D:\\addfixUtils\\1 .apk -o D:\\addfixUtils\\output -k D:\\addfixUtils\\key.jks -p 000000 -a liuyu -e 000000
其他 如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。
刚开始做的demo中,每次产生的apatch文件用的名字都是相同的,结果导致只有第一次的补丁能生效。看了源码后发现只有每次名字不同才能加载,log中应该也有提示,但是没注意到。
由于原理类似dexposed,所以目前发现activity的生命周期方法中不要直接hotfix.推荐在initview/initdata之类的方法中进行.
常见问题http://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.Tokj1O&treeId=234&articleId=105843&docType=1
阿里百川http://www.tuicool.com/articles/viEJfeE 主要好处是可以对补丁很好的管理,例如停止发布、继续发布、发布回滚等等