IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 关于解决Android系统开发者选项关闭动画后ValueAnimator不生效的问题 -> 正文阅读

[游戏开发]关于解决Android系统开发者选项关闭动画后ValueAnimator不生效的问题

一、前言

由于用户在开发者选项中关闭了动画的效果,导致App的动画不生效。但是有的App动画效果被关闭了,还依旧能有动画效果产生,这就让我们的用户觉得这是个BUG😀。于是,就有这个问题的解决方案。

开发者选项关闭动画如下:

(关于为什么用户要关闭动画效果?baidu一下,原因竟然是因为能提升手机的流畅度😭😭😭?)

二、如何解决?

工具类

public class ValueAnimatorUtil {

    private static String TAG = "ValueAnimatorUtil";

    /**
     * 如果动画被禁用,则重置动画缩放时长
     */
    public static void resetDurationScaleIfDisable() {
        if (getDurationScale() == 0)
            resetDurationScale();
    }

    /**
     * 重置动画缩放时长
     */
    public static void resetDurationScale() {
        try {
            getField().setFloat(null, 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static float getDurationScale() {
        try {
            return getField().getFloat(null);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }

    @NonNull
    private static Field getField() throws NoSuchFieldException {
        Field field = ValueAnimator.class.getDeclaredField("sDurationScale");
        field.setAccessible(true);
        return field;
    }
}

怎么使用?

在Activity的onCreate方法中调用如下代码:

ValueAnimatorUtil.resetDurationScaleIfDisable()

?注意:以上代码只能在 targetSdkVersion为28或者以下生效,原因如下:

?可以看到,在我们需要反射的字段sDurationScale,多了UnsupportedAppUsage注解。

这是因为Google官方在Android P(Android9.0版本)开始针对非 SDK 接口的限制。也就是说,以前你系统的接口通过反射就能调用,现在都不能通过反射获取。

具体被限制的接口名单可以查看这里

所谓道高一尺魔高一丈,既然Google限制了我们,那就肯定会有大佬去解决问题,一起往下看如何突破Android P 接口限制。

三、如何突破Android P接口限制?

如何突破的原理可以看最后提供的参考链接,这篇文章主要讲如何使用。

第一步:将下面仓库地址代码添加到你的根 build.gradle 中:

allprojects {
?? ??? ?repositories {
?? ??? ??? ?...
?? ??? ??? ?maven { url 'https://jitpack.io' }
?? ??? ?}
?? ?}

第二步:将下面代码添加到你的主模块build.gradle中:

implementation 'com.github.tiann:FreeReflection:3.1.0'

第三步:在你的Application中加入如下代码:

public class MyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Reflection.unseal(base);
    }
}

最后,重新编译运行,在targetSdkVersion为28以上的版本都能修改sDurationScale成功。

四、分析源码

突破Andriod P限制仓库地址

可以把上面第三方SDK源码下载到本地,可以发现,核心代码量就十几行。主要文件就两个

BootstrapClassReflection,其中关于CPP的代码应该是旧的突破Android P限制的方法。
源码截图如下:

源码分析?

源码核心目的:

  • 通过反射调用?VMRuntime.setHiddenApiExemptions,把我们需要解除限制的方法传入,就能将我们自己要使用的隐藏 API 全部都豁免掉了。
  • 我们所有Java方法类的签名都是以?L开头,只要直接传个?L进去,所有隐藏API就会被全部解除限制。

开始从源码分析他是如何使用的

第一步:先从 Reflection.unseal(base)调用开始

public static int unseal(Context context) {
        if (SDK_INT < 28) {
            // Below Android P, ignore
            return 0;
        }

        // try exempt API first.
        if (exemptAll()) {
            return 0;
        }
        if (unsealByDexFile(context)) {
            return 0;
        }

        return -1;
    }

可以看到,主要方法其实就两个,exemptAllunsealByDexFile,先看下exemptAll方法。

第二步:exemptAll方法

public static boolean exemptAll() {
        return exempt(new String[]{"L"});
    }

可以看到,传入了一个字符串“L“,而这个L就是代表了所有的Java方法。继续往下看:

public static boolean exempt(String... methods) {
        if (sVmRuntime == null || setHiddenApiExemptions == null) {
            return false;
        }

        try {
            setHiddenApiExemptions.invoke(sVmRuntime, new Object[]{methods});
            return true;
        } catch (Throwable e) {
            return false;
        }
    }

?其实就是一个反射的调用setHiddenApiExemptions方法,我们看下setHiddenApiExemptions和sVnRuntime是如何获取的

public final class BootstrapClass {

    private static final String TAG = "BootstrapClass";

    private static Object sVmRuntime;
    private static Method setHiddenApiExemptions;

    static {
        if (SDK_INT >= Build.VERSION_CODES.P) {
            try {
                Method forName = Class.class.getDeclaredMethod("forName", String.class);
                Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);

                Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
                Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
                setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
                sVmRuntime = getRuntime.invoke(null);
            } catch (Throwable e) {
                Log.w(TAG, "reflect bootstrap failed:", e);
            }
        }
    }

上面代码的目的是为了获取到?setHiddenApiExemptions,由于setHiddenApiExemptions是属于系统的方法,普通的用户是无法获取到的。dalvik.system.VMRuntime源码截图如下:

/**
     * Sets the list of exemptions from hidden API access enforcement.
     *
     * @param signaturePrefixes
     *         A list of signature prefixes. Each item in the list is a prefix match on the type
     *         signature of a blacklisted API. All matching APIs are treated as if they were on
     *         the whitelist: access permitted, and no logging..
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public native void setHiddenApiExemptions(String[] signaturePrefixes);

注释里也说明了Sets the list of exemptions from hidden API access enforcement(设置隐藏 API 访问强制执行的豁免列表。)

需要我们把自己变成系统类,变成系统类的的核心代码主要就是这两句:

Method forName = Class.class.getDeclaredMethod("forName", String.class);
Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
  • 第一行,通过Class获取到自己的forName,用来反射获取类
  • 第二行,通过Class获取到自己的getDeclaredMethod方法,用来反射调用方法?

看到这里你可能会有疑惑,通过反射去反射获取类的方法获取到getDeclaredMethod,然后再去反射调用其他方法,那为什么不直接通过getDeclaredMethod反射调用,还要多此一步呢?

这是因为我们之所以在Android P以上不能调用被系统隐藏的类,是因为我们不是系统类。也就是说,我们把自己变成系统类,那么我们就不会存在有接口的限制。

上面可能说的有点绕。简单的说,通过上面两行代码,你就能变成系统类,那么你就能获取。

提示:上面这个方法在Android 11及以上已经失效(在小米8和一加8的安卓版本10上测试通过),🤭实际调用的是下面的unsealByDexFile方法。

第三步:unsealByDexFile方法

代码如下:

    private static boolean unsealByDexFile(Context context) {
        //1.解码DEX字段,获取字节流
        byte[] bytes = Base64.decode(DEX, Base64.NO_WRAP);
        //2.将获取的字节流,写入到本地文件
        File codeCacheDir = getCodeCacheDir(context);
        if (codeCacheDir == null) {
            return false;
        }
        File code = new File(codeCacheDir, System.currentTimeMillis() + ".dex");
        try {
            try (FileOutputStream fos = new FileOutputStream(code)) {
                fos.write(bytes);
            }
            //3.执行自己生成的dex文件
            DexFile dexFile = new DexFile(code);
            //4.加载BootstrapClass类
            Class<?> bootstrapClass = dexFile.loadClass("me.weishu.reflection.BootstrapClass", null);
            //5.调用exemptAll方法
            Method exemptAll = bootstrapClass.getDeclaredMethod("exemptAll");
            return (boolean) exemptAll.invoke(null);
    }

?可以看到,这个方法的最终还是调用exemptAll。这个方法的作用主要是通过系统类去加载我们自己的Dex文件,而这个Dex文件其实就是我们FreeRelfection的源码。生成的Dex文件截图如下:

?说到这里,那我们能不能也自己生成一个?继续往下看

五、生成Dex文件

1.拷贝源码到自己项目下,如图(包名com.example.viewdemo):

?2.将com.example.viewdemo下的代码打包成dex文件

这里可以看我另外一篇文章安卓中将Java文件转换成Dex文件_I'm a Android Dev的博客-CSDN博客

最终生成Hello.dex文件,也可以放到Android Studio打开查看。如下:

3.将Hello.dex文件编码成字符串

由于转换需要在Java工程里,需要创建一个Java工程。?可以将Hello.dex文件放在一起,截图如下:

注意:由于需要用到Android的Base64文件,所以需要拷贝Android的Base64文件到此。

代码如下:

public class FileUtil {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                String path = "D:\\20210426\\code\\otherCode\\ViewDemo\\lib\\src\\main\\java\\com\\example\\lib\\Hello.dex";
                byte[] bytesByFile = getBytesByFile(path);
                String DEX = Base64.encodeToString(bytesByFile, Base64.NO_WRAP);
                System.out.println("DEX: " + DEX);
            }
        }.start();
    }

    public static byte[] getBytesByFile(String pathStr) {
        File file = new File(pathStr);
        System.out.println("文件大小为: " + file.length());
        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
            byte[] b = new byte[1024];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            byte[] data = bos.toByteArray();
            bos.close();
            return data;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

运行后如下:

?将自己生成的DEX字符串拷贝到Reflection替换DEX字段,并且修改包名。如下:

private static final String DEX = "ZGV4CjAzNQDdv1qibNAiRzwtLS+hegZRh/tYwo8ujFOA。。。。。省略";

Class<?> bootstrapClass = dexFile.loadClass("com.example.viewdemo.BootstrapClass", null);

?最后运行App,设置依然生效。

本篇分享到此结束,说个题外话,文章中的获取元反射方法和DEX文件的生成,这些都是跟安卓的插件化息息相关的。学习需要互相联系。

参考链接:

Android ValueAnimator时长错乱或者不起作用的解决方法以及问题分析_陈小缘的博客-CSDN博客_valueanimator回调不正常

下面两篇是维术大佬关于Android P的限制的原理和解决方案,YYDS。

另一种绕过 Android P以上非公开API限制的办法 | Weishu's Notes

一种绕过Android P对非SDK接口限制的简单方法 | Weishu's Notes

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 13:05:27  更:2022-05-09 13:05:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 11:19:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码