所谓的系统级异常,是系统BUG,存在某些Android版本中,Android版本的升级一方面是加入新的特性,另一方面也是在修复这些系统级BUG,但某些时候,我们并没有办法通过系统升级来达到解决这种BUG。
下面介绍的这个系统级BUG,是存在于Android 7.x(SDK=24/25)版本当中,而我们店内设备大多都是Android 7 这个版本。
详细的异常信息:
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ba9eb53 is not valid; is your activity running? at android.view.ViewRootImpl.setView(ViewRootImpl.java:679) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94) at android.widget.Toast$TN.handleShow(Toast.java:459) at android.widget.Toast$TN$2.handleMessage(Toast.java:342) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:158) at android.app.ActivityThread.main(ActivityThread.java:6175) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783) 复制代码
Toast.makeText(context,“抱歉,网络异常,请……”, 1).show() 复制代码
Toast是一个UI展示工具类,调用它,会在界面上弹出一个提示框,我们一般会用它来做一些业务提示。这个异常就是在调用show()方法触发的。
在Android 7.x版本,handleShow()方法实现如下:
public void handleShow(IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { ... mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); .... mParams.token = windowToken; ...//这行代码出现异常 mWM.addView(mView, mParams); ... } } 复制代码
public void handleShow(IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { ... mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); .... mParams.token = windowToken; ... try {//修复了这个异常 mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ } ... } } 复制代码
try { Toast.makeText(context,“抱歉,网络异常,请……”, 1).show() } catch(..) { ... } 复制代码
不可以,这个异常无法通过异常捕获处理,因为show()方法是个异步的过程,因为show方法并不是真正调用handleShow方法的最初入口,show方法是用handler发送了一个消息,所以在调用show的地方通过异常捕获是无效的。
通过对Toast源码的分析,发现内部的handler变量是可以被替换的。
mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; break; } case CANCEL: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } }; 复制代码
通过反射,我们将Toast的内部的mHandler变量包装一下,更确切的说是替换Handler中的mCallback变量,是对mCallback成员变量进行包装,替换成我们自己的包装类。
private Toast workaround(Toast toast) { //1、反射获取到mHandler final Object handler = getFieldValue(tn, "mHandler"); if (handler instanceof Handler) { 2、替换为自定义包装类CaughtCallback,替换Handler中的mCallback if (setFieldValue(handler, "mCallback", new CaughtCallback((Handler) handler))) { return toast; } } return null; } 复制代码
public class CaughtCallback implements Handler.Callback { private final Handler mHandler; public CaughtCallback(final Handler handler) { this.mHandler = handler; } @Override public boolean handleMessage(final Message msg) { try { this.mHandler.handleMessage(msg); } catch (final RuntimeException e) { // ignore } return true; } } 复制代码
CaughtCallback做的事情非常的简单,当被执行handlerMessage时,会转调Toast中handler的handlerMessage方法,只不过在外部包了一层try catch,这样Toast中所有handlerMessage的地方被调用都会被异常处理。
public class FixToast{ private static final String TAG = "FixToast"; private static Toast toast; @SuppressLint("ShowToast") public static FixToast makeText(Context context, CharSequence text, int duration) { toast = Toast.makeText(context, "FixToast:" + text, duration); return new FixToast(); } public void show() { if(toast == null) { throw new RuntimeException("请先调用makeText方法"); } if (Build.VERSION.SDK_INT == 25) { workaround(toast).show(); } else { toast.show(); } } private Toast workaround(Toast toast) { final Object handler = getFieldValue(tn, "mHandler"); if (handler instanceof Handler) { if (setFieldValue(handler, "mCallback", new CaughtCallback((Handler) handler))) { return toast; } } return null; } } 复制代码
Handler在Android中的应用场景,从系统级到应用级都离不开它的身影
作为一个工具类,Toast是可以在子线程中被调用的,在Android里,耗时的操作是不可以在UI线程中执行的,我们将耗时操作放在一个线程中执行,当执行完成后需要通过一些UI来展示运行结果,比如可以通过Toast来展示一个提示框,如果不使用Handler是无法在子线程里对UI进行操作。
ActivityServiceManager是一个系统进程,我们的应用中的main Activity就是被ActivityServiceManager启动的,
Activity-> ActivityThread -> IBinder -> ActivityServiceManager
ActivityServiceManager-> IBinder -> ActivityThread -> Handler -> onCreate
public final class ActivityThread{ .... final H mH = new H(); .... private class H extends Handler { .... .... public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; case RELAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart"); ActivityClientRecord r = (ActivityClientRecord)msg.obj; handleRelaunchActivity(r); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; case PAUSE_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); handlePauseActivity((IBinder)msg.obj, false, (msg.arg1&1) != 0, msg.arg2, (msg.arg1&2) != 0); maybeSnapshot(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; ..... } ..... ..... } } 复制代码
Android中几种常见的插桩方式
像AspectJ这类框架本身即负责找到切面,也负责编织代码,我们接下来介绍的Gradle的Transform只是在hock编译中class到dex的过程,然后通过ASM对我们感兴趣的class进行编织。
上面已经介绍来如何修复系统BUG,以及介绍了Toast内部的Handler 机制,下面我将使用AOP的方式对项目中所有使用到Toast的代码进行修改。
用Gradle构建Android工程的主要流程Task:
> Configure project :app > Task :clean UP-TO-DATE > Task :android-plugin:clean UP-TO-DATE > Task :app:clean > Task :app:checkDebugClasspath > Task :app:preBuild UP-TO-DATE > Task :app:preDebugBuild > Task :app:compileDebugAidl NO-SOURCE > Task :app:compileDebugRenderscript > Task :app:checkDebugManifest > Task :app:generateDebugBuildConfig > Task :app:mainApkListPersistenceDebug > Task :app:generateDebugResValues > Task :app:generateDebugResources > Task :app:mergeDebugResources > Task :app:createDebugCompatibleScreenManifests > Task :app:processDebugManifest > Task :app:splitsDiscoveryTaskDebug > Task :app:processDebugResources > Task :app:compileDebugKotlin > Task :app:prepareLintJar UP-TO-DATE > Task :app:generateDebugSources > Task :app:javaPreCompileDebug > Task :app:compileDebugJavaWithJavac > Task :app:compileDebugNdk NO-SOURCE > Task :app:compileDebugSources > Task :app:mergeDebugShaders > Task :app:compileDebugShaders > Task :app:generateDebugAssets > Task :app:mergeDebugAssets > Task :app:transformClassesWithDexBuilderForDebug > Task :app:transformDexArchiveWithDexMergerForDebug > Task :app:mergeDebugJniLibFolders > Task :app:transformNativeLibsWithMergeJniLibsForDebug > Task :app:transformNativeLibsWithStripDebugSymbolForDebug > Task :app:checkDebugLibraries > Task :app:processDebugJavaRes NO-SOURCE > Task :app:transformResourcesWithMergeJavaResForDebug > Task :app:validateSigningDebug > Task :app:packageDebug > Task :app:assembleDebug 复制代码
Task :app:transformClassesWithDexBuilderForDebug,这个Task是开始对class转换为dex,所以如果要修改class,要在这个任务执行前执行一个自定义的任务,Gradle提供了一个这样的缺口,可以对构建流程进行Hock,这个API就是Transform,如果要使用Transform功能,必须要依赖自定义Gradle plugin。
class ToastPlugin : Transform(), Plugin<Project> { override fun getName(): String = "toastFix" override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS override fun isIncremental(): Boolean = true override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT override fun apply(project: Project) { println("===============ToastFix plugin apply===============" + project.plugins.hasPlugin("com.android.application") + project.plugins.hasPlugin("com.android.library")) when { project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library") -> { project.getAndroid<AppExtension>().registerTransform(this@ToastPlugin) println("===============ToastFix plugin registerTransform ===============") } } } override fun transform(transformInvocation: TransformInvocation) { println("===============ToastFix plugin start===============") transformInvocation.inputs.forEach { transformInput -> transformInput.directoryInputs.forEach { //获取输出dir val outputDir = transformInvocation.outputProvider.getContentLocation( it.file.absolutePath, it.contentTypes, it.scopes, Format.DIRECTORY) val inputDir = it.file //获取outputDir下所有的class文件 FileUtils.getAllFiles(inputDir).forEach {inputFile -> val bytes = inputFile.readBytes() val string = String(bytes) //待输出待文件 val outputFile = File(outputDir, inputFile.name) if(!outputFile.exists()) { outputFile.parentFile.mkdir() } if(string.contains("android/widget/Toast")) { //通过ASM处理class文件 } else { //不需要做处理的class outputFile.writeBytes(bytes) } } } } } } 复制代码
当toastFix任务被添加后的构建过程:
> Task :app:compileDebugSources > Task :app:mergeDebugShaders > Task :app:compileDebugShaders > Task :app:generateDebugAssets > Task :app:mergeDebugAssets > Task :app:transformClassesWithToastFixForDebug > Task :app:transformClassesWithDexBuilderForDebug > Task :app:transformDexArchiveWithDexMergerForDebug > Task :app:mergeDebugJniLibFolders > Task :app:transformNativeLibsWithMergeJniLibsForDebug > Task :app:transformNativeLibsWithStripDebugSymbolForDebug > Task :app:checkDebugLibraries 复制代码
接下来通过ASM框架修改目标class
Toast.makeText(context,“抱歉,网络异常,请……”, 1).show() FixToast.makeText(context,“抱歉,网络异常,请……”, 1).show() 复制代码
public class static main(String[] args) { int a = 1; int b = 2; int c = a +b; } 复制代码
文件中的为16进制代码, 文件开头的4个字节称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为 1.8.0。 通过java -version 命令稍加验证, 可得结果。
使用到java内置的一个反编译工具javap 可以反编译字节码文件
我们可以看到main方法的方法声明,descriptor说明这个方法的参数是一个字符串数组([Ljava/lang/String; ),而且返回类型是void(V)。下面的flags这行说明该方法是公开的(ACC_PUBLIC)和静态的 (ACC_STATIC)。
Code属性是最重要的部分,它包含了这个方法的一系列指令和信息,这些信息包含了操作栈的最大深度(本例中是2)和在这个方法的这一帧中被分配的本地变量的数量(本例中是4)。所有的本地变量在上面的指令中都提到了,除了第一个变量(索引为0),这个变量保存的是args参数。其他三个本地变量就相当于源码中的a,b和c。
关于字节码的类型对应如下:
www.wangyuwei.me/2017/01/19/…
public class ToastUtils { public static void doToast(Context context) { Toast.makeText(context,"im system toast", Toast.LENGTH_LONG).show(); } } 复制代码
private fun classVisitor(inputFile: File, bytes: ByteArray, outputFile: File) { val reader = ClassReader(bytes) val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS) val cv = ToastVisitor(ASM5, writer) reader.accept(cv, ClassReader.EXPAND_FRAMES) writer.visitEnd() val array = writer.toByteArray() outputFile.writeBytes(array) } 复制代码
class ToastVisitor(p0: Int, cv: ClassVisitor?) : ClassVisitor(p0, cv), Opcodes { val TOAST_CLASS = "com/jd/daintree/lichen/FixToast" private var injectUtil: Boolean = false//标记正在对Util类进行注入,还是对调用者进行注入 private var simpleClassName: String? = null override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<String>?) { cv.visit(version, access, name, signature, superName, interfaces) println("Doing logcat injection on class $name") } override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<String>?): MethodVisitor { //这里是对调用者的操作 return TestMethodVisitor(Opcodes.ASM5, mv) } override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { return super.visitAnnotation(descriptor, visible) } override fun visitField(access: Int, name: String?, descriptor: String?, signature: String?, value: Any?): FieldVisitor { return super.visitField(access, name, descriptor, signature, value) } } 复制代码
class ToastMethodVisitor(api: Int, methodVisitor: MethodVisitor?) : MethodVisitor(api, methodVisitor) { val TOAST_CLASS = "android/widget/Toast" val FIX_TOAST_CLASS = "com/jd/daintree/lichen/FixToast" override fun visitCode() { //方法体内开始时调用 super.visitCode() } override fun visitInsn(opcode: Int) { //每执行一个指令都会调用 super.visitInsn(opcode) } override fun visitMethodInsn(opcode: Int, owner: String?, name: String?, descriptor: String?, isInterface: Boolean) { var desc = descriptor var owner = owner //覆盖方法调用的过程 if (opcode == Opcodes.INVOKESTATIC && owner == TOAST_CLASS) { desc = "(Landroid/content/Context;Ljava/lang/CharSequence;I)L$FIX_TOAST_CLASS;"//方法描述符替换为影子方法 owner = FIX_TOAST_CLASS } if(name == "show") { owner = FIX_TOAST_CLASS } super.visitMethodInsn(opcode, owner, name, desc, isInterface) } } 复制代码
正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化,比如在某个包下的所有类中的某一类方法中都需要解决一个相似的问题,可以通过AOP的编程方式对此进行模块化封装,统一解决。关于AOP的具体解释,可以参照维基百科。而AspectJ就是面向切面编程在Java中的一种具体实现。 AspectJ向Java引入了一个新的概念——join point,它包括几个新的结构: pointcuts,advice,inter-type declarations 和 aspects。 join point是在程序流中被定义好的点。pointcut在那些点上选出特定的join point和值。advice是到达join point时被执行的代码。 AspectJ还具有不同类型的类型间声明(inter-type declarations),允许程序员修改程序的静态结构,即其类的成员和类之间的关系。 AspectJ中的几个名词术语解释
Cross-cutting concerns:即使在面向对象编程中大多数类都是执行一个单一的、特定的功能,它们也有时候需要共享一些通用的辅助功能。比如我们想要在一个线程进入和退出一个方法时,在数据层和UI层加上输出log的功能。尽管每一个类的主要功能时不同的,但是它们所需要执行的辅助功能是相似的。
Advice:需要被注入到.class字节码文件的代码。通常有三种:before,after和around,分别是在目标方法执行前,执行后以及替换目标代码执行。除了注入代码到方法中外,更进一步的,你还可以做一些别的修改,例如添加成员变量和接口到一个类中。
Join point:程序中执行代码插入的点,例如方法调用时或者方法执行时。
Pointcut:告诉代码注入工具在哪里注入特定代码的表达式(即需要在哪些Joint point应用特定的Advice)。它可以选择一个这样的点(例如,一个单一方法的执行)或者许多相似的点(例如,所有被自定义注解@DebugTrace标记的方法)。
Aspect: Aspect将pointcut和advice 联系在一起。例如,我们通过定义一个pointcut和给出一个准确的advice实现向我们的程序中添加一个打印日志功能的aspect。
Weaving:向目标位置(join point)注入代码(advice)的过程。
作者:Doctor明 链接: juejin.im/post/58f38e…
public class CustomerActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_page); findViewById(R.id.button).setOnClickListener(view -> { String s = "on button click"; System.out.prinln(s); }); } } 复制代码
@Aspect public class SneakAspect { @Pointcut("execution(void *..View.OnClickListener+.onClick(..))") public void onClicks() {} /** * 原生的onClick * @param point * @param view */ @After("onClicks() && args(view)") public void doOnClicks (JoinPoint point, View view){ try { sendClickEvent(view); } catch (Exception e) { System.out.println(e); } } } 复制代码