无需Root,无需反编译,用VirtualUETool查看修改任意App的布局参数

Posted zhangke3016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无需Root,无需反编译,用VirtualUETool查看修改任意App的布局参数相关的知识,希望对你有一定的参考价值。

UETool是饿了么推出一款开源库,已经出来一段时间了,用来帮助设计师,程序员,测试人员来在APP上修改View的各项参数。使用起来也很方便,但它只能在自己项目里引入依赖来使用,也就是说用它只能查看自己APP的布局位置信息。如果可以用它来查看手机上安装的任意APP,那是不是很酷呢?我们今天的目标就是:扩展UETool让它成为一个SuperUETool。先说下我们超级工具**VirtualUETool**,无需修改其他应用apk,无需反编译apk,无需手机Root,即拿即用,在Github已开源,欢迎star、fork哈~说了这么多,我们先看下效果吧:

接下来,我们来聊聊实现思路以及实现过程中遇到的问题,重点在于思路和想法的扩展,希望给你也有新的启发。
先说下本文的行文思路:

一、UETool工作原理梳理
二、VirtualUETool框架的实现思路梳理

我们这里的介绍重点在于UETool以及对其的改造,对VirtualApp实现插件化功能就不做过多阐述了哈

好了,那我们开始吧。

一、UETool工作原理梳理

UETool的基本使用就不说了,看下官方文档就很清楚了,基本使用在当前页面调用下UETool.showUETMenu这个方法就可以了。既然我们要开始改造UETool,
那我们接下来的重点就聊聊这个东西它的内部实现是什么样的,也方便我们后续的修改嘛。
首先从UETool.showUETMenu往下看
UETool.showMenu

private boolean showMenu(int y) 
        //检查开启悬浮窗权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
            if (!Settings.canDrawOverlays(Application.getApplicationContext())) 
                requestPermission(Application.getApplicationContext());
                Toast.makeText(Application.getApplicationContext(), "After grant this permission, re-enable UETool", Toast.LENGTH_LONG).show();
                return false;
            
        
        //启动UETool悬浮窗
        if (uetMenu == null) 
            uetMenu = new UETMenu(Application.getApplicationContext(), y);
        
        uetMenu.show();
        return true;
    

这里主要是申请悬浮窗权限,就不说了。后面下看UETMenu的构造方法,这个UETMenu是一个继承了LinearLayout的普通布局控件,构造方法中主要是初始化UI相关,看下关键部分:
UETMenu构造方法中

public class UETMenu extends LinearLayout

...
subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_catch_view), R.drawable.uet_edit_attr, new OnClickListener() 
            @Override
            public void onClick(View v) 
                //查看view属性
                open(TransparentActivity.Type.TYPE_EDIT_ATTR);
            
        ));
        subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_relative_location), R.drawable.uet_relative_position,
                new OnClickListener() 
                    @Override
                    public void onClick(View v) 
                        //查看view布局位置
                        open(TransparentActivity.Type.TYPE_RELATIVE_POSITION);
                    
                ));
        subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_grid), R.drawable.uet_show_gridding,
                new OnClickListener() 
                    @Override
                    public void onClick(View v) 
                        //显示网格栅栏,方便查看控件是否对齐
                        open(TransparentActivity.Type.TYPE_SHOW_GRIDDING);
                    
                ));
...

这里添加进悬浮窗点击展开的三部分,分别是查看view属性、查看view布局位置、显示网格栅栏这三个部分。OK,继续往下,就到了uetMenu.show()这里,

public void show() 
        try 
            windowManager.addView(this, getWindowLayoutParams());
         catch (Exception e) 
            e.printStackTrace();
        
    

就是往WindowManager中添加了UETMenu这个ViewGroup。接下来我们关注的重点来了,当点击各个功能按钮后统一都调用了open方法,往下走。

private void open(@TransparentActivity.Type int type) 
        Activity currentTopActivity = Util.getCurrentActivity();
        if (currentTopActivity == null) 
            return;
         else if (currentTopActivity.getClass() == TransparentActivity.class) 
            currentTopActivity.finish();
            return;
        
        //启动透明activity
        Intent intent = new Intent(currentTopActivity, TransparentActivity.class);
        intent.putExtra(TransparentActivity.EXTRA_TYPE, type);
        currentTopActivity.startActivity(intent);
        currentTopActivity.overridePendingTransition(0, 0);
        UETool.getInstance().setTargetActivity(currentTopActivity);
    

这里启动了一个透明的Activity,用于显示我们显示绘制布局信息和响应我们的手指点击,看重点

TransparentActivity.java

switch (type) 
            case TYPE_EDIT_ATTR:
                EditAttrLayout editAttrLayout = new EditAttrLayout(this);
                editAttrLayout.setOnDragListener(new EditAttrLayout.OnDragListener() 
                    @Override
                    public void showOffset(String offsetContent) 
                        board.updateInfo(offsetContent);
                    
                );
                vContainer.addView(editAttrLayout);
                break;
            case TYPE_RELATIVE_POSITION:
                vContainer.addView(new RelativePositionLayout(this));
                break;
            case TYPE_SHOW_GRIDDING:
                vContainer.addView(new GriddingLayout(this));
                board.updateInfo("LINE_INTERVAL: " + DimenUtil.px2dip(GriddingLayout.LINE_INTERVAL, true));
                break;
            default:
                Toast.makeText(this, getString(R.string.uet_coming_soon), Toast.LENGTH_SHORT).show();
                finish();
                break;
        

这里我们看到不同的功能在界面添加了不同的Layout,那接下来就分别分析下咯。
EditAttrLayoutRelativePositionLayout都继承自CollectViewsLayout,先来看下它们的爸爸~

@Override
    protected void onAttachedToWindow() 
        super.onAttachedToWindow();
        try 
            Activity targetActivity = UETool.getInstance().getTargetActivity();
            WindowManager windowManager = targetActivity.getWindowManager();
            Field mGlobalField = Class.forName("android.view.WindowManagerImpl").getDeclaredField("mGlobal");
            mGlobalField.setAccessible(true);

            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) 
                Field mViewsField = Class.forName("android.view.WindowManagerGlobal").getDeclaredField("mViews");
                mViewsField.setAccessible(true);
                List<View> views = (List<View>) mViewsField.get(mGlobalField.get(windowManager));
                for (int i = views.size() - 1; i >= 0; i--) 
                    View targetView = getTargetDecorView(targetActivity, views.get(i));
                    if (targetView != null) 
                        //获取当前显示的view
                        traverse(targetView);
                        break;
                    
                
             else 
                Field mRootsField = Class.forName("android.view.WindowManagerGlobal").getDeclaredField("mRoots");
                mRootsField.setAccessible(true);
                List viewRootImpls;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) 
                    viewRootImpls = (List) mRootsField.get(mGlobalField.get(windowManager));
                 else 
                    viewRootImpls = Arrays.asList((Object[]) mRootsField.get(mGlobalField.get(windowManager)));
                
                for (int i = viewRootImpls.size() - 1; i >= 0; i--) 
                    Class clazz = Class.forName("android.view.ViewRootImpl");
                    Object object = viewRootImpls.get(i);
                    Field mWindowAttributesField = clazz.getDeclaredField("mWindowAttributes");
                    mWindowAttributesField.setAccessible(true);
                    Field mViewField = clazz.getDeclaredField("mView");
                    mViewField.setAccessible(true);
                    View decorView = (View) mViewField.get(object);
                    WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mWindowAttributesField.get(object);
                    if (layoutParams.getTitle().toString().contains(targetActivity.getClass().getName())
                            || getTargetDecorView(targetActivity, decorView) != null) 
                        traverse(decorView);
                        break;
                    
                
            
         catch (Exception e) 
            e.printStackTrace();
        
    

    //递归遍历界面所有view并添加进elements集合中
    private void traverse(View view) 
        //如果在过滤的列表中,忽略
        if (UETool.getInstance().getFilterClasses().contains(view.getClass().getName())) return;
        //如果View不显示 忽略
        if (view.getAlpha() == 0 || view.getVisibility() != View.VISIBLE) return;
        //如果view tag  == DESABLE_UETOOL  忽略
        if (getResources().getString(R.string.uet_disable).equals(view.getTag())) return;
        elements.add(new Element(view));
        if (view instanceof ViewGroup) 
            ViewGroup parent = (ViewGroup) view;
            for (int i = 0; i < parent.getChildCount(); i++) 
                traverse(parent.getChildAt(i));
            
        
    

onAttachedToWindow方法中查找到当前界面显示的View并且递归遍历子View,添加至elements集合中,每个Element中保存由当前View的位置信息和其父级Element
继续看EditAttrLayout,这个控件用于显示当前View属性内容,主要看下这里:

//当点击某个控件位置时 会调用 triggerActionUp
class ShowMode implements IMode 

        @Override
        public void triggerActionUp(final MotionEvent event) 
            final Element element = getTargetElement(event.getX(), event.getY());
            if (element != null) 
                targetElement = element;
                invalidate();
                if (dialog == null) 
                    dialog = new AttrsDialog(getContext());
                    dialog.setAttrDialogCallback(new AttrsDialog.AttrDialogCallback() 
                        @Override
                        public void enableMove() 
                            mode = new MoveMode();
                            dialog.dismiss();
                        

                        @Override
                        public void showValidViews(int position, boolean isChecked) 
                            int positionStart = position + 1;
                            if (isChecked) 
                                dialog.notifyValidViewItemInserted(positionStart, getTargetElements(lastX, lastY), targetElement);
                             else 
                                dialog.notifyItemRangeRemoved(positionStart);
                            
                        

                        @Override
                        public void selectView(Element element) 
                            targetElement = element;
                            dialog.dismiss();
                            dialog.show(targetElement);
                        
                    );
                    dialog.setOnDismissListener(new DialogInterface.OnDismissListener() 
                        @Override
                        public void onDismiss(DialogInterface dialog) 
                            if (targetElement != null) 
                                targetElement.reset();
                                invalidate();
                            
                        
                    );
                
                dialog.show(targetElement);
            
        
    
    //当移动某个控件位置时  会调用 triggerActionMove 方法
    class MoveMode implements IMode 
    
            @Override
            public void triggerActionMove(MotionEvent event) 
                if (targetElement != null) 
                    boolean changed = false;
                    View view = targetElement.getView();
                    float diffX = event.getX() - lastX;
                    if (Math.abs(diffX) >= moveUnit) 
                        view.setTranslationX(view.getTranslationX() + diffX);
                        lastX = event.getX();
                        changed = true;
                    
                    float diffY = event.getY() - lastY;
                    if (Math.abs(diffY) >= moveUnit) 
                        view.setTranslationY(view.getTranslationY() + diffY);
                        lastY = event.getY();
                        changed = true;
                    
                    if (changed) 
                        targetElement.reset();
                        invalidate();
                    
                
            
        

这里抽象出公共的行为,不同行为操作单独处理实现,代码很简洁。从上面可以看到,在点击控件的时候,有一个AttrsDialog弹窗显示,也就是我们看到的显示控件实现的dialog,瞅瞅瞅瞅~
重点看下列表的adapter实现:

public void notifyDataSetChanged(Element element) 
            items.clear();
            for (String attrsProvider : UETool.getInstance().getAttrsProvider()) 
                try 
                    IAttrs attrs = (IAttrs) Class.forName(attrsProvider).newInstance();
                    items.addAll(attrs.getAttrs(element));
                 catch (Exception e) 
                    e.printStackTrace();
                
            
            notifyDataSetChanged();
        

当adapter的notifyDataSetChanged方法执行时,会从UETool.getInstance().getAttrsProvider()这里来拿我们希望支持的属性,框架默认支持了一部分基础属性,我们也可以通过
UETool.putAttrsProviderClass(String customizeClassName)来添加自定义支持的属性。先看下默认支持的怎么处理的:

    private Set<String> attrsProviderSet = new LinkedHashSet<String>() 
        
            add(UETCore.class.getName());
        
    ;

UETCore.java

    @Override
    public List<Item> getAttrs(Element element) 
        List<Item> items = new ArrayList<>();

        View view = element.getView();

        items.add(new SwitchItem("Move", element, SwitchItem.Type.TYPE_MOVE));
        items.add(new SwitchItem("ValidViews", element, SwitchItem.Type.TYPE_SHOW_VALID_VIEWS));

        IAttrs iAttrs = AttrsManager.createAttrs(view);
        if (iAttrs != null) 
            items.addAll(iAttrs.getAttrs(element));
        

        items.add(new TitleItem("COMMON"));
        items.add(new TextItem("Class", view.getClass().getName()));
        items.add(new TextItem("Id", Util.getResId(view)));
        items.add(new TextItem("ResName", Util.getResourceName(view.getId())));
        items.add(new TextItem("Clickable", Boolean.toString(view.isClickable()).toUpperCase()));
        items.add(new TextItem("Focused", Boolean.toString(view.isFocused以上是关于无需Root,无需反编译,用VirtualUETool查看修改任意App的布局参数的主要内容,如果未能解决你的问题,请参考以下文章

有没有一种简单的方法可以修改反编译文件而无需处理其依赖关系?

反编译工具jad下载安装及使用(无需集成环境一键使用)

Android运行C/C++程序,无需ROOT!

如何防止Unity3D代码被反编译

Android 无线调试设备,无需Root方式

adb wifi 测试(无需root)