组件化 模块化:项目按照独立的模块进行划分
项目组件化的重要环节在于,将项目按照模块来进行拆分,拆分成一个个业务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
组件 B 的开发人员再编写好组件 B 之后,需要提供接口文档
组件 A 的开发人员想用组件 B 的服务时,就需要根据接口文档有关于组件 B 的服务描述中,找到组件 B 所提供的服务的名字,然后调用 router.getService(服务全路径名);即可获取响应的服务
而面向接口编程其实说的是每个应用到服务的地方都是针对父类编程,以达到接口和实现的分离目的
由于在开发期是面对存放在componentservice目录下的服务父类编程,而在提供服务的组件中的IApplicationLike中注册了组件中关于服务的具体实现。
彻底明白 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 ; } } }
资源文件 Resources类就像公司的销售,而AssetManager类就像公司的研发。销售对外,研发基本不对外。 如图7-1所示,我们看到Resources类对外提供getString, getText, getDrawable 等各种方法,但其实都是间接调用AssetManager的私有方法,AssetManager 负责向Android系统要资源。
使用 AssetManager addAssetPath 方法,把所有插件的资源都合并到宿主的资源中 AssetManager 中有一个 addAssetPath(String path) 方法,App 启动的时候,会把当前 apk 的路径传进去,接下来 AssetManager 和 Resources 就能访问当前 apk 的所有资源了。addAssetPath 方法是不对外的,我们可以通过反射的方式,把插件 apk 的路径传人这个方法,那么就把插件资源添加到一个资源池中了。当前 App 的资源已经在这个池子中了。
ClassLoader
ClassLoader有PathClassLoader、DexClassLoader俩个子类
路径不同
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 的返回对象。
odex
PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 可以指定 optimizedDirectory ,也就是 dex2oat 的产物 .odex (优化后的 dex 文件)存放的位置,而 PathClassLoader 只能使用系统默认位置。但是这个 optimizedDirectory 在 Android 8.0 以后也被舍弃了,只能使用系统默认的位置了。
DEX 文件同级目录下添加一个 oat/ 文件作为 .odex 的存储目录。
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; }
热更新 热修复(热更新)主要用来修复代码、修复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这个数组的前面(插入到后面的话会被忽略掉)
热更更新和插件化的区别
插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
插件化在代码中有固定的入口,而热更新则可能改变任何一个位置的代码,热更新的原理ClassLoader 的 dex 文件替换,直接修改字节码。
手写热更新
创建一个新的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 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 ); PathClassLoader newClassLoader = new PathClassLoader (apk.getPath(), null ); Object newPathListObject = pathListField.get(newClassLoader); Object newDexElementsObject = dexElementsField.get(newPathListObject); Object dexElementsObject = dexElementsField.get(pathListObject); 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 主要好处是可以对补丁很好的管理,例如停止发布、继续发布、发布回滚等等