转载

AOP在Android中的应用

所谓的系统级异常,是系统BUG,存在某些Android版本中,Android版本的升级一方面是加入新的特性,另一方面也是在修复这些系统级BUG,但某些时候,我们并没有办法通过系统升级来达到解决这种BUG。

下面介绍的这个系统级BUG,是存在于Android 7.x(SDK=24/25)版本当中,而我们店内设备大多都是Android 7 这个版本。

WindowManager$BadTokenExceptionToken失效异常

AOP在Android中的应用

详细的异常信息:

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)
复制代码

1、Toast引发的异常的原因,以及修复方案

1.1、引起这个异常的地方

Toast.makeText(context,“抱歉,网络异常,请……”, 1).show()
复制代码

Toast是一个UI展示工具类,调用它,会在界面上弹出一个提示框,我们一般会用它来做一些业务提示。这个异常就是在调用show()方法触发的。

看一下Toast的源码,找到这个异常抛出的地方

在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);
        ...
    }
}
复制代码

在Android 8.0版本,修复了这个BUG,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;
        ...
        try {//修复了这个异常
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }
        ...
    }
}
复制代码

既然Android官方通过try catch解决,我们可以吗?

try {
    Toast.makeText(context,“抱歉,网络异常,请……”, 1).show()
} catch(..) {
    ...
}
复制代码

不可以,这个异常无法通过异常捕获处理,因为show()方法是个异步的过程,因为show方法并不是真正调用handleShow方法的最初入口,show方法是用handler发送了一个消息,所以在调用show的地方通过异常捕获是无效的。

1.2、通过反射处理异常

通过对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;
}
复制代码

自定义包装类CaughtCallback

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的地方被调用都会被异常处理。

FixToast.java

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;
    }
}
复制代码

2、Handle机制简介

Handler在Android中的应用场景,从系统级到应用级都离不开它的身影

  • APP的启动过程

  • Activity、Service组件启动机制及生命周期管理

  • 线程间通信,主要是指UI线程和子线程间通信

  • RxJava For Android的线程调度,EventBus底层实现

先搞懂一个问题:Toast为什么要使用Handler?

作为一个工具类,Toast是可以在子线程中被调用的,在Android里,耗时的操作是不可以在UI线程中执行的,我们将耗时操作放在一个线程中执行,当执行完成后需要通过一些UI来展示运行结果,比如可以通过Toast来展示一个提示框,如果不使用Handler是无法在子线程里对UI进行操作。

AOP在Android中的应用

Activity对onCreate是被谁直接调用的?

ActivityServiceManager是一个系统进程,我们的应用中的main Activity就是被ActivityServiceManager启动的,

调用时序

Activity-> ActivityThread -> IBinder -> ActivityServiceManager

ActivityServiceManager-> IBinder -> ActivityThread -> Handler -> onCreate

AOP在Android中的应用
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;
                       .....
             }
             .....
             .....
         }
    }
复制代码

3、AOP方案,Gradle的Transform的使用

Android中几种常见的插桩方式

AOP在Android中的应用

像AspectJ这类框架本身即负责找到切面,也负责编织代码,我们接下来介绍的Gradle的Transform只是在hock编译中class到dex的过程,然后通过ASM对我们感兴趣的class进行编织。

使用代码插桩及代码编织的库

  • Spring

  • RxJava

  • Retrofit

  • ButterKnife

  • Dagger

  • Hugo

  • EventBus

  • 插件化框架

回顾一下

上面已经介绍来如何修复系统BUG,以及介绍了Toast内部的Handler 机制,下面我将使用AOP的方式对项目中所有使用到Toast的代码进行修改。

对Toast修复为什么要使用AOP?

  • 项目中使用Toast的地方多,替换繁琐。

  • 第三方库中的使用Toast的代码无法修改。

  • 不需要投入精力在代码规范的维护上

通过Transform对代码“偷梁换柱”

用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。

自定义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()
复制代码

4、Java字节码简介

public class static main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a +b;
}
复制代码
AOP在Android中的应用

文件中的为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 可以反编译字节码文件

AOP在Android中的应用

我们可以看到main方法的方法声明,descriptor说明这个方法的参数是一个字符串数组([Ljava/lang/String; ),而且返回类型是void(V)。下面的flags这行说明该方法是公开的(ACC_PUBLIC)和静态的 (ACC_STATIC)。

Code属性是最重要的部分,它包含了这个方法的一系列指令和信息,这些信息包含了操作栈的最大深度(本例中是2)和在这个方法的这一帧中被分配的本地变量的数量(本例中是4)。所有的本地变量在上面的指令中都提到了,除了第一个变量(索引为0),这个变量保存的是args参数。其他三个本地变量就相当于源码中的a,b和c。

从地址0到8的指令将执行以下操作:

  • iconst_1:将整形常量1放入操作数栈。
  • istore_1:在索引为1的位置将第一个操作数出栈(一个int值)并且将其存进本地变量,相当于变量a。
  • iconst_2:将整形常量2放入操作数栈。
  • istore_2:在索引为2的位置将第一个操作数出栈并且将其存进本地变量,相当于变量b。
  • iload_1:从索引1的本地变量中加载一个int值,放入操作数栈。
  • iload_2:从索引2的本地变量中加载一个int值,放入操作数栈。
  • iadd:把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。
  • istore_3:在索引为3的位置将第一个操作数出栈并且将其存进本地变量,相当于变量c。
  • return:从这个void方法中返回。
    AOP在Android中的应用

关于字节码的类型对应如下:

AOP在Android中的应用

www.wangyuwei.me/2017/01/19/…

通过安装ASM Bytecode Viewer插件来查看字节码文件

public class ToastUtils {
    public static void  doToast(Context context) {
        Toast.makeText(context,"im system toast", Toast.LENGTH_LONG).show();
    }
}
复制代码
AOP在Android中的应用
AOP在Android中的应用
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)
    }
}
复制代码

6、AOP方案,AspectJ介绍

AspectJ是什么

正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化,比如在某个包下的所有类中的某一类方法中都需要解决一个相似的问题,可以通过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…

Sample

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);
        }
    }
}
复制代码
原文  https://juejin.im/post/5d5254075188253a8a3bda6f
正文到此结束
Loading...