1、背景
在之前的一篇文章里已经分享过了如何使用Android Studio + Gradle编译整机APK[https://blog.csdn.net/skysukai/article/details/86616309], 既然app已经可以脱离整机码源编译了,那是否可以走得更远些,将工程中的framework.jar给移除掉呢?答案是有可能的。
为什么会使用framework.jar的API而不直接使用Android提供的API呢?因为framework中还有许多接口未对外开放,在Google的官方叫法中,这些API叫non-sdk interface,这些非sdk的接口只在framework中被使用(也就是Android整机代码),未将他们开放出来当成普通的sdk interface。比如常见的SystemProperties就是一个非sdk接口。
APP是一款文件管理器,基本功能就是提供文件的快速访问。如果要将代码中依赖的framework.jar给移除掉,首先要解决的问题就是在什么地方使用了framework的API。
2、确定工程中使用framework API的地方
在build.gradle文件中注释掉编译framework.jar的地方,这样就能快速得到代码中哪些地方使用了framework中的API。
import android.os.UserHandle;
import android.os.SystemProperties;
import android.os.storage.VolumeInfo;
……
上面给出的import就是使用了framework中非sdk接口,这样我们就快速知道了使用非sdk接口的地方,接下来要做的就是确认这些非sdk接口的使用场景。确认完毕之后,就可以开始修改了。
3、详细步骤
3.1、使用sdk接口替换非sdk接口
在确认了每一个调用非sdk接口的地方之后,接下来需要确认是否有sdk的接口可以替换非sdk的接口。
import android.content.ServiceConnection;
import android.media.IMediaScannerListener;
import android.media.IMediaScannerService;
比如这几个非sdk接口,他们的作用就是绑定到MediaScannerService
上,完成文件扫描的工作,这几个接口是否有sdk接口来完成相同工作呢?MediaScannerConnection
就是这个类。MediaScannerConnection提供了onScanCompleted这个回调,可以用这个回调来通知界面刷新,完成的功能和bind到MediaScannerService完成的工作是一致的。接下来的工作就很简单了,即按照原有的流程,使用sdk接口来替代非sdk接口完成的工作。这部分代码修改起来比较零散且修改较大,这里就不贴代码了。
3.2、使用反射
并不是每一个非sdk接口都有这么好的运气,有对应的sdk接口完成相同的工作。这时候只能使用反射,代码运行的时候反射到framework中。下面给出APP中使用到的VolumeInfo
反射调用,VolumeInfo的作用主要是封装了一些存储信息,在文件管理器这类APP中使用较多。
/**
* This class is reflected to 'android.os.storage.VolumeInfo'.
* We use some non-SDK interfaces.
*/
public class VolumeInfoReflection {
public static final String ACTION_VOLUME_STATE_CHANGED =
"android.os.storage.action.VOLUME_STATE_CHANGED";
public static final String EXTRA_VOLUME_ID =
"android.os.storage.extra.VOLUME_ID";
public static final String EXTRA_VOLUME_STATE =
"android.os.storage.extra.VOLUME_STATE";
/** Stub volume representing internal private storage */
public static final String ID_PRIVATE_INTERNAL = "private";
/** Real volume representing internal emulated storage */
public static final String ID_EMULATED_INTERNAL = "emulated";
public static final int TYPE_PUBLIC = 0;
public static final int TYPE_PRIVATE = 1;
public static final int TYPE_EMULATED = 2;
public static final int TYPE_ASEC = 3;
public static final int TYPE_OBB = 4;
public static final int STATE_UNMOUNTED = 0;
public static final int STATE_CHECKING = 1;
public static final int STATE_MOUNTED = 2;
public static final int STATE_MOUNTED_READ_ONLY = 3;
public static final int STATE_FORMATTING = 4;
public static final int STATE_EJECTING = 5;
public static final int STATE_UNMOUNTABLE = 6;
public static final int STATE_REMOVED = 7;
public static final int STATE_BAD_REMOVAL = 8;
//反射一个方法
public static List<?> getVolumeInfos(Context context) {
StorageManager mStorageManager = context.getSystemService(StorageManager.class);
List<?> volumeInfos = new ArrayList<>();
try {
Class storeManagerClazz = Class.forName("android.os.storage.StorageManager");
Method getVolumesMethod = storeManagerClazz.getMethod("getVolumes");
volumeInfos = (List<?>) getVolumesMethod.invoke(mStorageManager);
} catch (Exception e) {
e.printStackTrace();
} finally {
return volumeInfos;
}
}
//反射一个字段
public static int getTypeField(Object volumeInfo) {
int type = -1;
try {
Class volumeInfoClazz = Class.forName("android.os.storage.VolumeInfo");
Field fsTypeField = volumeInfoClazz.getDeclaredField("type");
type = (int)fsTypeField.get(volumeInfo);
} catch (Exception e) {
e.printStackTrace();
} finally {
return type;
}
}
public static File getPath(Object volumeInfo) {
String path = getPathField(volumeInfo);
return (path != null) ? new File(path) : null;
}
//反射一个字段
public static String getPathField(Object volumeInfo) {
String path = "";
try {
Class volumeInfoClazz = Class.forName("android.os.storage.VolumeInfo");
Field pathField = volumeInfoClazz.getDeclaredField("path");
path = (String) pathField.get(volumeInfo);
} catch (Exception e) {
e.printStackTrace();
} finally {
return path;
}
}
//反射一个方法
public static Object getDiskMethod(Object volumeInfo) {
Object diskInfo = null;
try {
Class volumeInfoClazz = Class.forName("android.os.storage.VolumeInfo");
Method getDiskMethod = volumeInfoClazz.getMethod("getDisk");
diskInfo = getDiskMethod.invoke(volumeInfo);
} catch (Exception e) {
e.printStackTrace();
} finally {
return diskInfo;
}
}
……
4、收尾工作
将每一个使用到非sdk接口的地方修改完之后,需对APP进行全量遍历,防止由于修改导致功能回退及其他异常。
5、后记
Google对于非sdk接口的管控越来越严厉,据其官网上说,为什么不推荐使用这些非sdk接口,是因为这些非sdk接口可能会引发未知的异常。同时给出了非sdk接口的黑、白、灰名单。位于黑名单当中的非sdk接口即使使用反射也是没办法调用的,会抛出ClassNotFound Exception
; 位于灰名单当中的非sdk接口至少在Android9.0还可以通过反射调用,但是不保证更高版本的Android还能调用;而位于白名单当中的非sdk接口可以一直调用。这个名单大家感兴趣可以自行上网找找。而我的这次修改,通过反射调用的非sdk接口都已经在名单上检查过了,大多位于灰名单当中,少数一两个位于白名单当中,没有位于黑名单当中的,这也从另一个方面保证了这次修改的可行性。