Android--导入framework.jar

APK开发需要实现 选择系统语言 功能,使用反射和导入framework架包2种方法都可实现。

由于修改系统语言需要系统权限,所以无论使用哪种方法,都需要给APK添加系统权限,添加系统权限又必须添加系统签名,系统会要求该APK具有与系统相同的签名,否则会安装失败。

系统权限和系统签名

1. 添加系统权限

添加系统权限很简单,只需要在APK的AndroidManifest.xml中声明android:sharedUserId="android.uid.system"即可。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    // 添加系统权限
    android:sharedUserId="android.uid.system">
        ...
</manifest>

2. 添加系统签名

添加系统签名,首先需要将系统签名复制到工程目录下,在config.gradle中配置参数,并在工程的build.gradle中应用下:

<config.gradle>

ext {
    sign = [
            file         : '../system_key.jks',
            storePassword: 'xxxxxxxx',  
            keyAlias     : 'system_key',
            keyPassword  : 'xxxxxxxx'
    ]
}


<build.gradle>

apply from: "config.gradle"

然后在APP的build.gradle中打包时增加系统签名:

android{
    ...
    signingConfigs {
        hikvision {
            storeFile file(sign.file)
            storePassword sign.storePassword
            keyAlias sign.keyAlias
            keyPassword sign.keyPassword
        }
    }

    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.hikvision
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.hikvision
        }
    }
    ...
}
系统签名.jpg

3. 可能存在的问题

APK添加了系统权限,webView会报错

WebView is not allowed in privileged processes

Android 6.0 以上不允许在拥有系统权限的应用中使用 WebView,需要在onCreate()方法中的setContentView()之前调用以下的hookWebView()方法:

    public static void hookWebView() {
        int sdkInt = android.os.Build.VERSION.SDK_INT;
        try {
            Class<?> factoryClass = Class.forName("android.webkit.WebViewFactory");
            Field field = factoryClass.getDeclaredField("sProviderInstance");
            field.setAccessible(true);
            Object sProviderInstance = field.get(null);
            if (sProviderInstance!= null) {
                Log.i(TAG, "sProviderInstance isn't null");
                return;
            }
            Method getProviderClassMethod = null;
            if (sdkInt > 22) {
                getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass");
            } else if (sdkInt == 22) {
                getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
            } else {
                Log.i(TAG, "Don't need to Hook WebView");
                return;
            }
            getProviderClassMethod.setAccessible(true);
            Class<?> factoryProviderClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
            Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate");
            Constructor<?> delegateConstructor = delegateClass.getDeclaredConstructor();
            delegateConstructor.setAccessible(true);
            if (sdkInt < 26) {
                Constructor<?> providerConstructor = factoryProviderClass.getConstructor(delegateClass);
                if (providerConstructor!= null) {
                    providerConstructor.setAccessible(true);
                    sProviderInstance = providerConstructor.newInstance(delegateConstructor.newInstance());
                }
            } else {
                Field chromiumMethodName = factoryClass.getDeclaredField("CHROMIUM_WEBVIEW_FACTORY_METHOD");
                chromiumMethodName.setAccessible(true);
                String chromiumMethodNameStr = (String) chromiumMethodName.get(null);
                if (chromiumMethodNameStr == null) {
                    chromiumMethodNameStr = "create";
                }
                Method staticFactory = factoryProviderClass.getMethod(chromiumMethodNameStr, delegateClass);
                if (staticFactory!= null) {
                    sProviderInstance = staticFactory.invoke(null, delegateConstructor.newInstance());
                }
            }

            if (sProviderInstance!= null) {
                field.set(null, sProviderInstance);
                Log.i(TAG, "Hook success!");
            } else {
                Log.i(TAG, "Hook failed!");
            }
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }

反射实现系统语言切换

获取到系统权限之后就可以通过反射方法拿到framework中的LocalePicker类,然后调用其中的updateLocale方法就可以实现系统语言切换。

<MainActivity.java>

    private final List<String> systemLanguageList = new ArrayList<String>(
            Arrays.asList(
                    "zh", "en", "fr", "es", "de", "it", "pt", "ru", "pl", "ar", "tr", "vi",
                    "hu", "nl", "ro", "cs", "bg", "uk", "hr", "sr", "el", "no", "da"
            ));

    private void initView(){
        findViewById(R.id.get_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getLanguage();
            }
        });

        findViewById(R.id.set_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                setLanguage(systemLanguageList.get(1));
            }
        });
    }

    // 获取当前系统语言
    private void getLanguage(){
        // 获取当前Activity的Locale
        Locale activityLocale = getResources().getConfiguration().locale;
        // 获取语言代码
        String language = activityLocale.getLanguage();
        Log.d("getLanguage", "current Language: " + language );
    }

    // 设置当前系统语言
    private void setLanguage(String language){
        try {
            Class localPicker = Class.forName("com.android.internal.app.LocalePicker");
            Method updateLocale = localPicker.getDeclaredMethod("updateLocale", Locale.class);
            updateLocale.invoke(null, new Locale(language, ""));
        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
            Log.d("error", "try setLanguage Exception " );
        }
    }

导入framework.jar

1. 拷贝framework.jar

首先,需要从源码中把framework.jar拷贝到本地工程app/libs下,可将源码中的jar更名为framework.jar

Android N/O:  7 和 8
out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar

Android P/Q:  9 和 10
out/soong/.intermediates/frameworks/base/framework/android_common/combined/framework.jar

Android R:  11以上
out/soong/.intermediates/frameworks/base/framework-minus-apex/android_common/combined/framework-minus-apex.jar
导入framework.jpg

2. Android Studio适配

.iml文件是IntelliJ IDEA和Android Studio用来存储模块级别的配置信息的文件,3.6.3以后的Android Stuido版本默认关闭 .iml 文件的生成,导入framework.jar需要配置使用的优先级,所有需要先把Android Stuido中生成 .iml 文件的功能打开。

Android Studio —> File —> Settings —> Build... —> Build Tools —> Gradle —> 勾选Generate *.iml files for modules imported from Gradle —> Apply —> OK —> 重启Android Studio

配置as.jpg

3. 修改build.gradle(:app)配置

(1)添加framework.jar依赖

compileOnly files('libs\\framework.jar')

(2)修改资源链接优先级

// 优先链接framework.jar
gradle.projectsEvaluated {
    // 方法一
    tasks.withType(JavaCompile) {
        Set<File> fileSet = options.bootstrapClasspath.getFiles();
        List<File> newFileList = new ArrayList<>()
        newFileList.add(new File("libs/framework.jar"))
        newFileList.addAll(fileSet)
        options.bootstrapClasspath = files(newFileList.toArray())
    }
    // 方法二
//    tasks.withType(JavaCompile).tap {
//        configureEach {
//            options.compilerArgs.add("-Xbootclasspath/p:$rootProject.rootDir/app/libs/framework.jar")
//        }
//    }
}

(3)修改类的使用优先级

// 降低Android SDK的使用级别,优先使用framework.jar中的类文件
preBuild {
    doLast {
        def rootProjectName = rootProject.name.replace(" ", "_")
        def projectName = project.name.replace(" ", "_")
        def iml_path = "$rootProject.rootDir\\.idea\\modules\\" + projectName + "\\" + rootProjectName + "." + projectName + ".main.iml"
        def imlFile = file(iml_path)
        try {
            // 如果AS未适配,这里会找不到XmlParser
            def parsedXml = (new XmlParser()).parse(imlFile)
            def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
            def sdkString = jdkNode.'@jdkName'
            parsedXml.component[1].remove(jdkNode)
            new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
            groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
        } catch (FileNotFoundException e) {
            e.printStackTrace()
        }
    }
}

(4)解决65536限制

MultiDex是Android开发中用于解决65536个方法限制的一种机制,当应用的代码量超过65536个方法时,需要使用MultiDex来将应用拆分成多个DEX文件。

android {
    ...
    defaultConfig {
        applicationId "com.android.kc9demo"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    ...
}

dependencies{
    ...
        implementation 'com.android.support:multidex:1.0.0'
}

(5)使用framework.jar中的方法

导入framework.jar之后,在MainActivity中调用updateLocale方法就可以直接使用了。

    private void setLanguage(String language){
        LocalePicker.updateLocale(new Locale(language, ""));
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容