深入理解PackageManagerService
2015-10-20 16:19
411 查看
http://blog.csdn.net/yujun411522/article/details/46226001
本文出自:【yujun411522的博客】
PackageManagerService负责管理系统的Package,包括APK的安装,卸载,信息的查询等等。它的功能非常的多,也非常的强大,所以要重点分析。
PMS(PackageManagerService)和java中其他系统服务一样,也是一个Service,它和它的Client的关系:
1 IPackageManager接口定义了server要提供的业务函数,其中子类Stub继承Binder且实现了IPackageManager接口
2 PMS继承Stub,所以可以作为Server与Binder通信
3 Stub中的一个内部类Proxy中有一个IBinder的成员变量mRemote,利用mRemote可以和Server端通信
4 client端在使用的时候是使用Context.getPackageManager函数返回的ApplicationPackageManager对象来处理,ApplicationPackageManager内部成员变量mPM指向Proxy类型的对象
可以看出java层的系统服务的模型都是一样的,换的仅仅是服务和服务的实现,关于java层系统服务,请看java层系统服务在binder中的实现
8.1 PMS的启动
PMS的启动非常的复杂,涉及到Setting对象,属性系统,Installer系统,PackageHandler,系统权限,AndroidManifest.xml,Resouce,FileObserver已经APK的安装包的扫面等等,具体来说就是下面的历程:
其中ServerThread的启动之前已经讨论过了,这里直接看PackageManagerService的main函数 :
[java]
view plaincopyprint?
public static final IPackageManager main(Context context, boolean factoryTest,
boolean onlyCore) {
PackageManagerService m = new PackageManagerService(context, factoryTest, onlyCore);
ServiceManager.addService("package", m);//添加java系统服务的功能
return m;
}
public static final IPackageManager main(Context context, boolean factoryTest, boolean onlyCore) { PackageManagerService m = new PackageManagerService(context, factoryTest, onlyCore); ServiceManager.addService("package", m);//添加java系统服务的功能 return m; }
因为添加服务到ServiceManager在之前的PowerManagerService中已经讨论过,这里不做介绍,这里只看PackageManagerService的创建,它的构造函数中做了很多的工作,这也是android启动慢的一个主要原因,分段来看这个函数的执行。
8.1.1 Setting对象的创建和初始化
....
[html]
view plaincopyprint?
mSettings = new Settings();
mSettings.addSharedUserLPw("android.uid.system",
Process.SYSTEM_UID, ApplicationInfo.FLAG_SYSTEM);
mSettings.addSharedUserLPw("android.uid.phone",
MULTIPLE_APPLICATION_UIDS
? RADIO_UID : FIRST_APPLICATION_UID,
ApplicationInfo.FLAG_SYSTEM);
mSettings.addSharedUserLPw("android.uid.log",
MULTIPLE_APPLICATION_UIDS
? LOG_UID : FIRST_APPLICATION_UID,
ApplicationInfo.FLAG_SYSTEM);
mSettings.addSharedUserLPw("android.uid.nfc",
MULTIPLE_APPLICATION_UIDS
? NFC_UID : FIRST_APPLICATION_UID,
ApplicationInfo.FLAG_SYSTEM);
mSettings = new Settings(); mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID, ApplicationInfo.FLAG_SYSTEM); mSettings.addSharedUserLPw("android.uid.phone", MULTIPLE_APPLICATION_UIDS ? RADIO_UID : FIRST_APPLICATION_UID, ApplicationInfo.FLAG_SYSTEM); mSettings.addSharedUserLPw("android.uid.log", MULTIPLE_APPLICATION_UIDS ? LOG_UID : FIRST_APPLICATION_UID, ApplicationInfo.FLAG_SYSTEM); mSettings.addSharedUserLPw("android.uid.nfc", MULTIPLE_APPLICATION_UIDS ? NFC_UID : FIRST_APPLICATION_UID, ApplicationInfo.FLAG_SYSTEM);
这段代码做了两个工作: 1 构造Settings对象 ;2 调用addSharedUserLPw函数添加共享ID
1 构造Settings对象
Settings类在frameworks/base/services/java/com/android/server/pm中,Settings类的作用是管理android系统运行过程中的一些配置信息,它的构造函数:
[html]
view plaincopyprint?
Settings() {
File dataDir = Environment.getDataDirectory();
File systemDir = new File(dataDir, "system");
systemDir.mkdirs();
FileUtils.setPermissions(systemDir.toString(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
//packages.xml 记录系统所有安装apk的信息
mSettingsFilename = new File(systemDir, "packages.xml");
//packages-backup.xml 是packages.xml备份文件,在安装或者卸载apk是更新packages.xml文件就会用backup备份
mBackupSettingsFilename = new File(systemDir, "packages-backup.xml");
//packages.list 所有已安装apk的简要信息
mPackageListFilename = new File(systemDir, "packages.list");
// packages-stopped.xml 强制stop的apk信息
mStoppedPackagesFilename = new File(systemDir, "packages-stopped.xml");
//packages-stopped.xml的备份文件
mBackupStoppedPackagesFilename = new File(systemDir, "packages-stopped-backup.xml");
}
Settings() { File dataDir = Environment.getDataDirectory(); File systemDir = new File(dataDir, "system"); systemDir.mkdirs(); FileUtils.setPermissions(systemDir.toString(), FileUtils.S_IRWXU|FileUtils.S_IRWXG |FileUtils.S_IROTH|FileUtils.S_IXOTH, -1, -1); //packages.xml 记录系统所有安装apk的信息 mSettingsFilename = new File(systemDir, "packages.xml"); //packages-backup.xml 是packages.xml备份文件,在安装或者卸载apk是更新packages.xml文件就会用backup备份 mBackupSettingsFilename = new File(systemDir, "packages-backup.xml"); //packages.list 所有已安装apk的简要信息 mPackageListFilename = new File(systemDir, "packages.list"); // packages-stopped.xml 强制stop的apk信息 mStoppedPackagesFilename = new File(systemDir, "packages-stopped.xml"); //packages-stopped.xml的备份文件 mBackupStoppedPackagesFilename = new File(systemDir, "packages-stopped-backup.xml"); }
它的构造函数还是很简单的,初始化几个全局变量文件,这些文件变量后面解析过程中会用到。
2 调用addSharedUserLPw函数添加共享ID
[html]
view plaincopyprint?
//定义了一个HashMap存储sharedUsers信息
final HashMap<String, SharedUserSetting> mSharedUsers =
new HashMap<String, SharedUserSetting>();
SharedUserSetting addSharedUserLPw(String name, int uid, int pkgFlags) {
//先在mSharedUsers中找有没有name对应的value。
SharedUserSetting s = mSharedUsers.get(name);//
if (s != null) {
if (s.userId == uid) {//和传入的uid一样就直接返回
return s;
}
..如果s.userId != ui ,返回null
return null;
}
//如果没有在mSharedUsers 找到该name对应的value值,则创建一个新的SharedUserSetting 对象
s = new SharedUserSetting(name, pkgFlags);
s.userId = uid;
//调用addUserIdLPw
if (addUserIdLPw(uid, s, name)) {
mSharedUsers.put(name, s);//添加到mSharedUsers 中
return s;//返回创建的SharedUserSetting 对象
}
return null;//addUserIdLPw 添加失败就返回null
}
//定义了一个HashMap存储sharedUsers信息 final HashMap<String, SharedUserSetting> mSharedUsers = new HashMap<String, SharedUserSetting>(); SharedUserSetting addSharedUserLPw(String name, int uid, int pkgFlags) { //先在mSharedUsers中找有没有name对应的value。 SharedUserSetting s = mSharedUsers.get(name);// if (s != null) { if (s.userId == uid) {//和传入的uid一样就直接返回 return s; } ..如果s.userId != ui ,返回null return null; } //如果没有在mSharedUsers 找到该name对应的value值,则创建一个新的SharedUserSetting 对象 s = new SharedUserSetting(name, pkgFlags); s.userId = uid; //调用addUserIdLPw if (addUserIdLPw(uid, s, name)) { mSharedUsers.put(name, s);//添加到mSharedUsers 中 return s;//返回创建的SharedUserSetting 对象 } return null;//addUserIdLPw 添加失败就返回null }
这里面又涉及到一个SharedUserSetting类,它的用途和AndroidManifest.xml文件中的一个标签相关android:sharedUserId
如android:sharedUserId="android.uid.system",如果在AndroidManifest.xml中这么配置的话,它有一下几个作用:1 有相同android:sharedUserId值的apk之间可以运行在同一个进程中 ;2 通过设置android:sharedUserId,该apk所在的进程的uid就是android:sharedUserId的uid,这里就是apk有system用户的权限。也即是这个进程现在运行在sharedUserId所在进程中,且有它的权限。
现在要存储某一个UID有哪些apk共享该UID需要以下三个字段:
1 String name:就是存储xml文件中android:sharedUserId的值,这里就是"android.uid.system"
2 int uid:在linux中uid是一个整数,这里对应的就是1000
3 HashSet<PackageSetting> packages:共享同一个UID的package很多 ,package设置就是通过PackageSetting类实现,它的超类是GrantedPermissions,它的pkgFlage就是用来存储package的标记。
这里涉及到Settings、GrantedPermissions、ShareUserSetting等类,他们之间的关系如下:
1.Settings类中有一个HashMap<String,SharedUserSetting>的成员变量mSharedUsers,其中key为String(比如 "android.uid.system")类型,它表示某一个共享用户name,value类型就是
SharedUserSetting类型,用来存储该共享该name的所有共享用户信息。
2.SharedUserSetting类继承GrantedPermissions,且内部维护一个HashSet<PackageSetting>的成员变量packages,表示有相同userId的package设置信息接着看addUserIdLPw
3.其中Settings中有两个成员变量ArrayList<Object>mUserIds,和SparseArray<Object> mOtherUserIds,其中mUserIds存储大于等于(10000)的SharedUserSetting对象,而mOtherUserIds存储小于10000的SharedUserSetting对象。下面会分析。
在addSharedUserLPw方法中调用了addUserIdLPw方法,这里以 mSettings.addSharedUserLPw("android.uid.system",Process.SYSTEM_UID, ApplicationInfo.FLAG_SYSTEM);为例,其中Process.SYSTEM_UID的值为1000
[html]
view plaincopyprint?
private boolean addUserIdLPw(int uid, Object obj, Object name) {
// 三个参数分别为 uid=1000
// obj= new SharedUserSetting("android.uid.system",ApplicationInfo.FLAG_SYSTEM),
// name = "android.uid.system"
// PackageManagerService.FIRST_APPLICATION_UID值为 10000
//PackageManagerService.MAX_APPLICATION_UIDS 值1000
//这里不能uid不能超过10000+1000=11000
//系统版本不一样这里的实现不一样
if (uid >= PackageManagerService.FIRST_APPLICATION_UID + PackageManagerService.MAX_APPLICATION_UIDS) {
return false;
}
//uid>=10000时
if (uid >= PackageManagerService.FIRST_APPLICATION_UID) {
int N = mUserIds.size();
final int index = uid - PackageManagerService.FIRST_APPLICATION_UID;
while (index >= N) {
mUserIds.add(null);
N++;
}
//可以看出mUserIds 中第i个位置,它存放的是uid=PackageManagerService.FIRST_APPLICATION_UID +i的SharedUserSetting 对象,如果没
//有就设为null。
if (mUserIds.get(index) != null) {//该位置已经设置过了
return false;//返回false
}
mUserIds.set(index, obj);//该位置为null,可以插入,返回true
} else {
//uid<10000时
if (mOtherUserIds.get(uid) != null) {同理这里也是只不过是存储小于10000的SharedUserSetting 对象
return false;
}
mOtherUserIds.put(uid, obj);
}
return true;
}
private boolean addUserIdLPw(int uid, Object obj, Object name) { // 三个参数分别为 uid=1000 // obj= new SharedUserSetting("android.uid.system",ApplicationInfo.FLAG_SYSTEM), // name = "android.uid.system" // PackageManagerService.FIRST_APPLICATION_UID值为 10000 //PackageManagerService.MAX_APPLICATION_UIDS 值1000 //这里不能uid不能超过10000+1000=11000 //系统版本不一样这里的实现不一样 if (uid >= PackageManagerService.FIRST_APPLICATION_UID + PackageManagerService.MAX_APPLICATION_UIDS) { return false; } //uid>=10000时 if (uid >= PackageManagerService.FIRST_APPLICATION_UID) { int N = mUserIds.size(); final int index = uid - PackageManagerService.FIRST_APPLICATION_UID; while (index >= N) { mUserIds.add(null); N++; } //可以看出mUserIds 中第i个位置,它存放的是uid=PackageManagerService.FIRST_APPLICATION_UID +i的SharedUserSetting 对象,如果没 //有就设为null。 if (mUserIds.get(index) != null) {//该位置已经设置过了 return false;//返回false } mUserIds.set(index, obj);//该位置为null,可以插入,返回true } else { //uid<10000时 if (mOtherUserIds.get(uid) != null) {同理这里也是只不过是存储小于10000的SharedUserSetting 对象 return false; } mOtherUserIds.put(uid, obj); } return true; }
所以addUserIdLPw函数就是判断指定的uid能否按照一定的规则插入到mUserIds或者mOtherUserIds中。
如果这个方法返回true,按照key="mSharedUsers" value=new SharedUserSetting("android.uid.system",ApplicationInfo.FLAG_SYSTEM) put到Settings的成员变量mSharedUsers中。
到此为止Settting初始化和addSharedUserLPw分析完毕
8.1.2 获取系统默认设置
这里比较简单,读取"ro.build.type"和"debug.separate_processes"进行一些设置
8.1.3 启动HandlerThread
[html]
view plaincopyprint?
mHandlerThread.start();
mHandler = new PackageHandler(mHandlerThread.getLooper());
mHandlerThread.start(); mHandler = new PackageHandler(mHandlerThread.getLooper());
其中涉及到了HandlerThread、PackageHandler等类,它们之间的关系就是:
可以看出HandlerThread和PackageHandler分别代表系统通信模型中的Looper线程和Handler处理器,其中PackageHandler是PMS的内部类,它的handleMessage方法的实现实际调用了doHandleMessage方法:
[html]
view plaincopyprint?
public void handleMessage(Message msg) {
try {
doHandleMessage(msg);//调用了doHandleMessage方法
} finally {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
}
void doHandleMessage(Message msg) {
switch (msg.what) {
case INIT_COPY:
......
case CHECK_PENDING_VERIFICATION:
case PACKAGE_VERIFIED:
}
}
public void handleMessage(Message msg) { try { doHandleMessage(msg);//调用了doHandleMessage方法 } finally { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); } } void doHandleMessage(Message msg) { switch (msg.what) { case INIT_COPY: ...... case CHECK_PENDING_VERIFICATION: case PACKAGE_VERIFIED: } }
这里的作用就是启动mHandlerThread,不断从消息队列中取出来消息,交给PackageHandler处理。后面的apk安装会涉及到这部分的功能。
8.2.4 初始化UserManager
[html]
view plaincopyprint?
mInstaller = new Installer();
mUserAppDataDir = new File(dataDir, "user");
mUserManager = new UserManager(mInstaller, mUserAppDataDir);
mInstaller = new Installer(); mUserAppDataDir = new File(dataDir, "user"); mUserManager = new UserManager(mInstaller, mUserAppDataDir);
创建UserManager对象,看它的构造方法:
[html]
view plaincopyprint?
public UserManager(Installer installer, File baseUserPath) {
this(Environment.getDataDirectory(), baseUserPath);
mInstaller = installer;
}
UserManager(File dataDir, File baseUserPath) {
//USER_INFO_DIR 为"system/users";
mUsersDir = new File(dataDir, USER_INFO_DIR);
//创建/data/system/users 目录
mUsersDir.mkdirs();
mBaseUserPath = baseUserPath;
FileUtils.setPermissions(mUsersDir.toString(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
// 指向/data/system/users userlist.xml文件
mUserListFile = new File(mUsersDir, USER_LIST_FILENAME);
readUserList();//调用readUserList函数
}
public UserManager(Installer installer, File baseUserPath) { this(Environment.getDataDirectory(), baseUserPath); mInstaller = installer; } UserManager(File dataDir, File baseUserPath) { //USER_INFO_DIR 为"system/users"; mUsersDir = new File(dataDir, USER_INFO_DIR); //创建/data/system/users 目录 mUsersDir.mkdirs(); mBaseUserPath = baseUserPath; FileUtils.setPermissions(mUsersDir.toString(), FileUtils.S_IRWXU|FileUtils.S_IRWXG |FileUtils.S_IROTH|FileUtils.S_IXOTH, -1, -1); // 指向/data/system/users userlist.xml文件 mUserListFile = new File(mUsersDir, USER_LIST_FILENAME); readUserList();//调用readUserList函数 }
先创建了/data/system/users 目录 ,然后调用readUserList 函数
[html]
view plaincopyprint?
private void readUserList() {
mUsers = new SparseArray<UserInfo>();
//如果/data/system/users userlist.xml文件 不存在
if (!mUserListFile.exists()) {
fallbackToSingleUser();
return;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(mUserListFile);
XmlPullParser parser = Xml.newPullParser();
parser.setInput(fis, null);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
;
}
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
//TAG_USER 为"user"
if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_USER)) {
//读取<user>标签中的id属性
String id = parser.getAttributeValue(null, ATTR_ID);
UserInfo user = readUser(Integer.parseInt(id));//根据id在读取xml构造成一个UserInfo实体
if (user != null) {
mUsers.put(user.id, user);//添加到mUsers
}
}
}
updateUserIds();
} catch (IOException ioe) {
fallbackToSingleUser();
} catch (XmlPullParserException pe) {
fallbackToSingleUser();
}
}
private void readUserList() { mUsers = new SparseArray<UserInfo>(); //如果/data/system/users userlist.xml文件 不存在 if (!mUserListFile.exists()) { fallbackToSingleUser(); return; } FileInputStream fis = null; try { fis = new FileInputStream(mUserListFile); XmlPullParser parser = Xml.newPullParser(); parser.setInput(fis, null); int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { ; } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { //TAG_USER 为"user" if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_USER)) { //读取<user>标签中的id属性 String id = parser.getAttributeValue(null, ATTR_ID); UserInfo user = readUser(Integer.parseInt(id));//根据id在读取xml构造成一个UserInfo实体 if (user != null) { mUsers.put(user.id, user);//添加到mUsers } } } updateUserIds(); } catch (IOException ioe) { fallbackToSingleUser(); } catch (XmlPullParserException pe) { fallbackToSingleUser(); } }
其中涉及到了readUser,它的作用就是读取/data/system/users/id.xml文件,将xml文件转化为UserInfo实体类
[html]
view plaincopyprint?
private UserInfo readUser(int id) {
int flags = 0;
String name = null;
FileInputStream fis = null;
try {
File userFile = new File(mUsersDir, Integer.toString(id) + ".xml");
fis = new FileInputStream(userFile);
XmlPullParser parser = Xml.newPullParser();
parser.setInput(fis, null);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
;
}
if (type != XmlPullParser.START_TAG) {
Slog.e(LOG_TAG, "Unable to read user " + id);
return null;
}
if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_USER)) {
String storedId = parser.getAttributeValue(null, ATTR_ID);
if (Integer.parseInt(storedId) != id) {
Slog.e(LOG_TAG, "User id does not match the file name");
return null;
}
String flagString = parser.getAttributeValue(null, ATTR_FLAGS);
flags = Integer.parseInt(flagString);
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
}
if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_NAME)) {
type = parser.next();
if (type == XmlPullParser.TEXT) {
name = parser.getText();
}
}
}
fis.close();
UserInfo userInfo = new UserInfo(id, name, flags);
return userInfo;
} catch (IOException ioe) {
} catch (XmlPullParserException pe) {
}
return null;
private UserInfo readUser(int id) { int flags = 0; String name = null; FileInputStream fis = null; try { File userFile = new File(mUsersDir, Integer.toString(id) + ".xml"); fis = new FileInputStream(userFile); XmlPullParser parser = Xml.newPullParser(); parser.setInput(fis, null); int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { ; } if (type != XmlPullParser.START_TAG) { Slog.e(LOG_TAG, "Unable to read user " + id); return null; } if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_USER)) { String storedId = parser.getAttributeValue(null, ATTR_ID); if (Integer.parseInt(storedId) != id) { Slog.e(LOG_TAG, "User id does not match the file name"); return null; } String flagString = parser.getAttributeValue(null, ATTR_FLAGS); flags = Integer.parseInt(flagString); while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { } if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_NAME)) { type = parser.next(); if (type == XmlPullParser.TEXT) { name = parser.getText(); } } } fis.close(); UserInfo userInfo = new UserInfo(id, name, flags); return userInfo; } catch (IOException ioe) { } catch (XmlPullParserException pe) { } return null; }
看一下UserInfo实体类
[html]
view plaincopyprint?
public class UserInfo implements Parcelable {
public static final int FLAG_PRIMARY = 0x00000001;
public static final int FLAG_ADMIN = 0x00000002;
public static final int FLAG_GUEST = 0x00000004;
public int id;//用户id
public String name;//用户名称
public int flags;//用户标记,是primary,admin,guest等等
public UserInfo(int id, String name, int flags) {
this.id = id;
this.name = name;
this.flags = flags;
}
public boolean isPrimary() {
return (flags & FLAG_PRIMARY) == FLAG_PRIMARY;
}
public boolean isAdmin() {
return (flags & FLAG_ADMIN) == FLAG_ADMIN;
}
public boolean isGuest() {
return (flags & FLAG_GUEST) == FLAG_GUEST;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int parcelableFlags) {
dest.writeInt(id);
dest.writeString(name);
dest.writeInt(flags);
}
public static final Parcelable.Creator<UserInfo> CREATOR
= new Parcelable.Creator<UserInfo>() {
public UserInfo createFromParcel(Parcel source) {
return new UserInfo(source);
}
public UserInfo[] newArray(int size) {
return new UserInfo[size];
}
};
private UserInfo(Parcel source) {
id = source.readInt();
name = source.readString();
flags = source.readInt();
}
}
public class UserInfo implements Parcelable { public static final int FLAG_PRIMARY = 0x00000001; public static final int FLAG_ADMIN = 0x00000002; public static final int FLAG_GUEST = 0x00000004; public int id;//用户id public String name;//用户名称 public int flags;//用户标记,是primary,admin,guest等等 public UserInfo(int id, String name, int flags) { this.id = id; this.name = name; this.flags = flags; } public boolean isPrimary() { return (flags & FLAG_PRIMARY) == FLAG_PRIMARY; } public boolean isAdmin() { return (flags & FLAG_ADMIN) == FLAG_ADMIN; } public boolean isGuest() { return (flags & FLAG_GUEST) == FLAG_GUEST; } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeInt(id); dest.writeString(name); dest.writeInt(flags); } public static final Parcelable.Creator<UserInfo> CREATOR = new Parcelable.Creator<UserInfo>() { public UserInfo createFromParcel(Parcel source) { return new UserInfo(source); } public UserInfo[] newArray(int size) { return new UserInfo[size]; } }; private UserInfo(Parcel source) { id = source.readInt(); name = source.readString(); flags = source.readInt(); } }
实现了Parcelable 接口保存user的一些信息。
如果readUser(id)的结果不为null就将该结果加入到mUsers中。
8.2.5 解析permission
readPermissions();函数调用的实现
[html]
view plaincopyprint?
void readPermissions() {
// 指向etc/permissions 文件
File libraryDir = new File(Environment.getRootDirectory(), "etc/permissions");
..
// Iterate over the files in the directory and scan .xml files
for (File f : libraryDir.listFiles()) {
//不处理etc/permissions/platform.xml
if (f.getPath().endsWith("etc/permissions/platform.xml")) {
continue;
}
//处理.xml文件
if (!f.getPath().endsWith(".xml")) {
continue;
}
//处理刻度的
if (!f.canRead()) {
continue;
}
readPermissionsFromXml(f);
}
// Read permissions from .../etc/permissions/platform.xml last so it will take precedence
最后解析etc/permissions/platform.xm 文件
final File permFile = new File(Environment.getRootDirectory(),"etc/permissions/platform.xml");
readPermissionsFromXml(permFile);
}
void readPermissions() { // 指向etc/permissions 文件 File libraryDir = new File(Environment.getRootDirectory(), "etc/permissions"); .. // Iterate over the files in the directory and scan .xml files for (File f : libraryDir.listFiles()) { //不处理etc/permissions/platform.xml if (f.getPath().endsWith("etc/permissions/platform.xml")) { continue; } //处理.xml文件 if (!f.getPath().endsWith(".xml")) { continue; } //处理刻度的 if (!f.canRead()) { continue; } readPermissionsFromXml(f); } // Read permissions from .../etc/permissions/platform.xml last so it will take precedence 最后解析etc/permissions/platform.xm 文件 final File permFile = new File(Environment.getRootDirectory(),"etc/permissions/platform.xml"); readPermissionsFromXml(permFile); }
可见就是解析各种/etc/permissions目录中的xml文件,最后才解析etc/permissions/platform.xml文件。不过我们先看etc/permissions/platform.xml文件,这是我手机上的platform.xml文件(一部分)
[html]
view plaincopyprint?
<permissions>
<!--为指定group 的gid分配相应的权限-->
<!--为指定gid为net_bt_admin分配 BLUETOOTH_ADMIN权限-->
<permission name="android.permission.BLUETOOTH_ADMIN" >
<group gid="net_bt_admin" />
</permission>
<permission name="android.permission.BLUETOOTH" >
<group gid="net_bt" />
</permission>
<permission name="android.permission.BLUETOOTH_STACK" >
<group gid="net_bt_stack" />
</permission>
<!--为指定的uid分配相应的权限-->
<!--为指定uid为 shell 分配WRITE_EXTERNAL_STORAGE权限-->
<assign-permission name="android.permission.WRITE_EXTERNAL_STORAGE" uid="shell" />
<assign-permission name="android.permission.SEND_SMS" uid="shell" />
<assign-permission name="android.permission.CALL_PHONE" uid="shell" />
<assign-permission name="android.permission.READ_CONTACTS" uid="shell" />
<assign-permission name="android.permission.WRITE_CONTACTS" uid="shell" />
<!--连接库-->
<library name="android.test.runner" file="/system/framework/android.test.runner.jar" />
<library name="javax.obex" file="/system/framework/javax.obex.jar"/>
</permissions>
<permissions> <!--为指定group 的gid分配相应的权限--> <!--为指定gid为net_bt_admin分配 BLUETOOTH_ADMIN权限--> <permission name="android.permission.BLUETOOTH_ADMIN" > <group gid="net_bt_admin" /> </permission> <permission name="android.permission.BLUETOOTH" > <group gid="net_bt" /> </permission> <permission name="android.permission.BLUETOOTH_STACK" > <group gid="net_bt_stack" /> </permission> <!--为指定的uid分配相应的权限--> <!--为指定uid为 shell 分配WRITE_EXTERNAL_STORAGE权限--> <assign-permission name="android.permission.WRITE_EXTERNAL_STORAGE" uid="shell" /> <assign-permission name="android.permission.SEND_SMS" uid="shell" /> <assign-permission name="android.permission.CALL_PHONE" uid="shell" /> <assign-permission name="android.permission.READ_CONTACTS" uid="shell" /> <assign-permission name="android.permission.WRITE_CONTACTS" uid="shell" /> <!--连接库--> <library name="android.test.runner" file="/system/framework/android.test.runner.jar" /> <library name="javax.obex" file="/system/framework/javax.obex.jar"/> </permissions>
解析platform.xml通过函数readPermissionsFromXml来实现,该函数的主要作用就是将permission 、group 、assign-permission,library 标签读出来并存储在相应的数据结构中。
上面的文件是platform.xml文件,还有一类文件是feature配置文件,包括硬件和软件 feature配置,先看硬件feature,这里android.hardware.wifi.xml为例:
[html]
view plaincopyprint?
<permissions>
<feature name="android.hardware.wifi" />
</permissions>
<permissions> <feature name="android.hardware.wifi" /> </permissions>
说明当前设备可以支持wifi
再看软件 feature配置android.software.live_wallpaper.xml
[html]
view plaincopyprint?
permissions>
<feature name="android.software.live_wallpaper" />
</permissions>
permissions> <feature name="android.software.live_wallpaper" /> </permissions>
8.2.6 解析package文件
mSettings.readLPw()方法,它的作用就是解析之前的packagexxx.xml文件,包括package.xml,package-backup.xml,package-stop.xml,package-stopback.xml这些文件,主要历程如下:
主要历程在第二部分解析packages.xml文件,保存到相应的数据结构之中。
8.2.7 dexopt优化
[html]
view plaincopyprint?
int scanMode = SCAN_MONITOR | SCAN_NO_PATHS | SCAN_DEFER_DEX;
if (mNoDexOpt) {
scanMode |= SCAN_NO_DEX;//不需要DEX优化
}
final HashSet<String> libFiles = new HashSet<String>();
mFrameworkDir = new File(Environment.getRootDirectory(), "framework");
mDalvikCacheDir = new File(dataDir, "dalvik-cache");
boolean didDexOpt = false;
String bootClassPath = System.getProperty("java.boot.class.path");
if (bootClassPath != null) {
String[] paths = splitString(bootClassPath, ':');
for (int i=0; i<paths.length; i++) {
try {
//判断是否需要dex优化,如果需要则加入到libFiles中
if (dalvik.system.DexFile.isDexOptNeeded(paths[i])) {
libFiles.add(paths[i]);
mInstaller.dexopt(paths[i], Process.SYSTEM_UID, true);
didDexOpt = true;
}
} catch (FileNotFoundException e) {
} catch (IOException e) {
}
}
} else {
}
//在platform.xml文件中存储library 标签的变量
if (mSharedLibraries.size() > 0) {
Iterator<String> libs = mSharedLibraries.values().iterator();
while (libs.hasNext()) {
String lib = libs.next();
try {
//判断是否需要dex优化
if (dalvik.system.DexFile.isDexOptNeeded(lib)) {
libFiles.add(lib);
mInstaller.dexopt(lib, Process.SYSTEM_UID, true);
didDexOpt = true;
}
} catch (FileNotFoundException e) {
} catch (IOException e) {
}
}
}
//对于framework-res.apk直接添加到libFiles中
libFiles.add(mFrameworkDir.getPath() + "/framework-res.apk");
//mFrameworkDir代表 /system/frameworks目录,优化里面的apk和jar文件
String[] frameworkFiles = mFrameworkDir.list();
if (frameworkFiles != null) {
for (int i=0; i<frameworkFiles.length; i++) {
.....
}
}
if (didDexOpt) {//如果didDexOpt为true,则说明优化成功
//mDalvikCacheDir代表 /data/dalvik-cache
String[] files = mDalvikCacheDir.list();
if (files != null) {
for (int i=0; i<files.length; i++) {
String fn = files[i];
//删除data@app@ 或者data@app-private@的文件
if (fn.startsWith("data@app@")|| fn.startsWith("data@app-private@")) {
(new File(mDalvikCacheDir, fn)).delete();//删除缓存数据
}
}
}
}
int scanMode = SCAN_MONITOR | SCAN_NO_PATHS | SCAN_DEFER_DEX; if (mNoDexOpt) { scanMode |= SCAN_NO_DEX;//不需要DEX优化 } final HashSet<String> libFiles = new HashSet<String>(); mFrameworkDir = new File(Environment.getRootDirectory(), "framework"); mDalvikCacheDir = new File(dataDir, "dalvik-cache"); boolean didDexOpt = false; String bootClassPath = System.getProperty("java.boot.class.path"); if (bootClassPath != null) { String[] paths = splitString(bootClassPath, ':'); for (int i=0; i<paths.length; i++) { try { //判断是否需要dex优化,如果需要则加入到libFiles中 if (dalvik.system.DexFile.isDexOptNeeded(paths[i])) { libFiles.add(paths[i]); mInstaller.dexopt(paths[i], Process.SYSTEM_UID, true); didDexOpt = true; } } catch (FileNotFoundException e) { } catch (IOException e) { } } } else { } //在platform.xml文件中存储library 标签的变量 if (mSharedLibraries.size() > 0) { Iterator<String> libs = mSharedLibraries.values().iterator(); while (libs.hasNext()) { String lib = libs.next(); try { //判断是否需要dex优化 if (dalvik.system.DexFile.isDexOptNeeded(lib)) { libFiles.add(lib); mInstaller.dexopt(lib, Process.SYSTEM_UID, true); didDexOpt = true; } } catch (FileNotFoundException e) { } catch (IOException e) { } } } //对于framework-res.apk直接添加到libFiles中 libFiles.add(mFrameworkDir.getPath() + "/framework-res.apk"); //mFrameworkDir代表 /system/frameworks目录,优化里面的apk和jar文件 String[] frameworkFiles = mFrameworkDir.list(); if (frameworkFiles != null) { for (int i=0; i<frameworkFiles.length; i++) { ..... } } if (didDexOpt) {//如果didDexOpt为true,则说明优化成功 //mDalvikCacheDir代表 /data/dalvik-cache String[] files = mDalvikCacheDir.list(); if (files != null) { for (int i=0; i<files.length; i++) { String fn = files[i]; //删除data@app@ 或者data@app-private@的文件 if (fn.startsWith("data@app@")|| fn.startsWith("data@app-private@")) { (new File(mDalvikCacheDir, fn)).delete();//删除缓存数据 } } } }
主要是对几个路径的apk和jar文件判断看是否需要dexopt优化,有java.boot.class.path、mSharedLibraries、/system/frameworks目录这几个目录下面机型操作,若有任意一个文件夹中有任意一个文件需要优化,删除 /data/dalvik-cache 目录下以data@app@或者data@app-private@文件
8.2.8 启动FileObserver监控APK文件目录
[html]
view plaincopyprint?
<span style="white-space:pre"> </span> //监控 system/framework目录
mFrameworkInstallObserver = new AppDirObserver(mFrameworkDir.getPath(), OBSERVER_EVENTS, true);
mFrameworkInstallObserver.startWatching();
scanDirLI(mFrameworkDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR,scanMode | SCAN_NO_DEX, 0);
//监控 system/app目录
mSystemAppDir = new File(Environment.getRootDirectory(), "app");
mSystemInstallObserver = new AppDirObserver(mSystemAppDir.getPath(), OBSERVER_EVENTS, true);
mSystemInstallObserver.startWatching();
scanDirLI(mSystemAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);
//监控 /vendor/app目录
mVendorAppDir = new File("/vendor/app");
mVendorInstallObserver = new AppDirObserver(mVendorAppDir.getPath(), OBSERVER_EVENTS, true);
mVendorInstallObserver.startWatching();
scanDirLI(mVendorAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);
mInstaller.moveFiles();
...
mAppInstallDir = new File(dataDir, "app");
//look for any incomplete package installations
ArrayList<PackageSetting> deletePkgsList = mSettings.getListOfIncompleteInstallPackagesLPr();
//clean up list
for(int i = 0; i < deletePkgsList.size(); i++) {
cleanupInstallFailedPackage(deletePkgsList.get(i));
}
deleteTempPackageFiles();
if (!mOnlyCore) {
mAppInstallObserver = new AppDirObserver(mAppInstallDir.getPath(), OBSERVER_EVENTS, false);
mAppInstallObserver.startWatching();
scanDirLI(mAppInstallDir, 0, scanMode, 0);
mDrmAppInstallObserver = new AppDirObserver(mDrmAppPrivateInstallDir.getPath(), OBSERVER_EVENTS, false);
mDrmAppInstallObserver.startWatching();
scanDirLI(mDrmAppPrivateInstallDir, PackageParser.PARSE_FORWARD_LOCK,scanMode, 0);
} else {
mAppInstallObserver = null;
mDrmAppInstallObserver = null;
}
<span style="white-space:pre"> </span> //监控 system/framework目录 mFrameworkInstallObserver = new AppDirObserver(mFrameworkDir.getPath(), OBSERVER_EVENTS, true); mFrameworkInstallObserver.startWatching(); scanDirLI(mFrameworkDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR,scanMode | SCAN_NO_DEX, 0); //监控 system/app目录 mSystemAppDir = new File(Environment.getRootDirectory(), "app"); mSystemInstallObserver = new AppDirObserver(mSystemAppDir.getPath(), OBSERVER_EVENTS, true); mSystemInstallObserver.startWatching(); scanDirLI(mSystemAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0); //监控 /vendor/app目录 mVendorAppDir = new File("/vendor/app"); mVendorInstallObserver = new AppDirObserver(mVendorAppDir.getPath(), OBSERVER_EVENTS, true); mVendorInstallObserver.startWatching(); scanDirLI(mVendorAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0); mInstaller.moveFiles(); ... mAppInstallDir = new File(dataDir, "app"); //look for any incomplete package installations ArrayList<PackageSetting> deletePkgsList = mSettings.getListOfIncompleteInstallPackagesLPr(); //clean up list for(int i = 0; i < deletePkgsList.size(); i++) { cleanupInstallFailedPackage(deletePkgsList.get(i)); } deleteTempPackageFiles(); if (!mOnlyCore) { mAppInstallObserver = new AppDirObserver(mAppInstallDir.getPath(), OBSERVER_EVENTS, false); mAppInstallObserver.startWatching(); scanDirLI(mAppInstallDir, 0, scanMode, 0); mDrmAppInstallObserver = new AppDirObserver(mDrmAppPrivateInstallDir.getPath(), OBSERVER_EVENTS, false); mDrmAppInstallObserver.startWatching(); scanDirLI(mDrmAppPrivateInstallDir, PackageParser.PARSE_FORWARD_LOCK,scanMode, 0); } else { mAppInstallObserver = null; mDrmAppInstallObserver = null; }
利用FileObserver来监控几个文件夹下面的apk文件,可以监听文件或者目录的添加或者删除事件并作出响应。
8.2.9 调用scanDirLI方法扫描并安装apk文件
上面的过程中出现了一个函数scanDirLI函数,它的实现比较复杂后面会进行介绍
8.2.10更新package文件
mSettings.writeLPr();方法来将mSetting中存储的package信息保存到响应的文件之中
8.3 Installer和Installd
Installer是java层的接口,Installd是init启动的守护进行,两者是client和server的关系,installer的api会转化为installd的命令,他们之间的关系:
8.3.1 Installer
这是java层提供的Installer类,提供apk安装和卸载过程中的操作
安装的过程调用install方法:
[html]
view plaincopyprint?
public int install(String name, int uid, int gid) {
StringBuilder builder = new StringBuilder("install");
builder.append(' ');
builder.append(name);
builder.append(' ');
builder.append(uid);
builder.append(' ');
builder.append(gid);
return execute(builder.toString());
}
public int install(String name, int uid, int gid) { StringBuilder builder = new StringBuilder("install"); builder.append(' '); builder.append(name); builder.append(' '); builder.append(uid); builder.append(' '); builder.append(gid); return execute(builder.toString()); }
先将name,uid,gid按照一定的规则生成一个字符串形式:"install name uid gid" 然后执行execute方法:
[html]
view plaincopyprint?
private int execute(String cmd) {
String res = transaction(cmd);//调用transaction 方法
try {
return Integer.parseInt(res);
} catch (NumberFormatException ex) {
return -1;
}
}
private int execute(String cmd) { String res = transaction(cmd);//调用transaction 方法 try { return Integer.parseInt(res); } catch (NumberFormatException ex) { return -1; } }
调用了transaction方法:
[html]
view plaincopyprint?
private synchronized String transaction(String cmd) {
if (!connect()) {//连接installd服务
Slog.e(TAG, "connection failed");
return "-1";
}
if (!writeCommand(cmd)) {//向installd发送命令
Slog.e(TAG, "write command failed? reconnect!");
if (!connect() || !writeCommand(cmd)) {
return "-1";
}
}
if (LOCAL_DEBUG) {
Slog.i(TAG, "send: '" + cmd + "'");
}
if (readReply()) {//读取返回的数据
String s = new String(buf, 0, buflen);
if (LOCAL_DEBUG) {
Slog.i(TAG, "recv: '" + s + "'");
}
return s;
} else {
if (LOCAL_DEBUG) {
Slog.i(TAG, "fail");
}
return "-1";
}
}
private synchronized String transaction(String cmd) { if (!connect()) {//连接installd服务 Slog.e(TAG, "connection failed"); return "-1"; } if (!writeCommand(cmd)) {//向installd发送命令 Slog.e(TAG, "write command failed? reconnect!"); if (!connect() || !writeCommand(cmd)) { return "-1"; } } if (LOCAL_DEBUG) { Slog.i(TAG, "send: '" + cmd + "'"); } if (readReply()) {//读取返回的数据 String s = new String(buf, 0, buflen); if (LOCAL_DEBUG) { Slog.i(TAG, "recv: '" + s + "'"); } return s; } else { if (LOCAL_DEBUG) { Slog.i(TAG, "fail"); } return "-1"; } }
三个过程: connect 进行连接;writeCommand 发送请求 ; readReply读取响应
可以看出来client端的工作仍然是由installd来完成,client将请求发送给installd来完成,真正的工作是installd
8.3.2 Installd服务
Intalld服务是init阶段启动的服务进行,在clieng发送一个命令之后会将该命令按照下面的对应方式进行映射:
[html]
view plaincopyprint?
struct cmdinfo {
const char *name; //命令名称
unsigned numargs; //参数个数
int (*func)(char **arg, char reply[REPLY_MAX]); //该命令对应的函数
};
struct cmdinfo cmds[] = {
{ "ping", 0, do_ping },
{ "install", 3, do_install },
{ "dexopt", 3, do_dexopt },
{ "movedex", 2, do_move_dex },
{ "rmdex", 1, do_rm_dex },
{ "remove", 2, do_remove },
{ "rename", 2, do_rename },
{ "freecache", 1, do_free_cache },
{ "rmcache", 1, do_rm_cache },
{ "protect", 2, do_protect },
{ "getsize", 4, do_get_size },
{ "rmuserdata", 2, do_rm_user_data },
{ "movefiles", 0, do_movefiles },
{ "linklib", 2, do_linklib },
{ "unlinklib", 1, do_unlinklib },
{ "mkuserdata", 3, do_mk_user_data },
{ "rmuser", 1, do_rm_user },
};
struct cmdinfo { const char *name; //命令名称 unsigned numargs; //参数个数 int (*func)(char **arg, char reply[REPLY_MAX]); //该命令对应的函数 }; struct cmdinfo cmds[] = { { "ping", 0, do_ping }, { "install", 3, do_install }, { "dexopt", 3, do_dexopt }, { "movedex", 2, do_move_dex }, { "rmdex", 1, do_rm_dex }, { "remove", 2, do_remove }, { "rename", 2, do_rename }, { "freecache", 1, do_free_cache }, { "rmcache", 1, do_rm_cache }, { "protect", 2, do_protect }, { "getsize", 4, do_get_size }, { "rmuserdata", 2, do_rm_user_data }, { "movefiles", 0, do_movefiles }, { "linklib", 2, do_linklib }, { "unlinklib", 1, do_unlinklib }, { "mkuserdata", 3, do_mk_user_data }, { "rmuser", 1, do_rm_user }, };
这里的命令是install,对应的函数是do_install,它有调用Commands.c中的install方法:
[html]
view plaincopyprint?
int install(const char *pkgname, uid_t uid, gid_t gid)
{
char pkgdir[PKG_PATH_MAX];
char libdir[PKG_PATH_MAX];
if ((uid < AID_SYSTEM) || (gid < AID_SYSTEM)) {
LOGE("invalid uid/gid: %d %d\n", uid, gid);
return -1;
}
if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, 0)) {
LOGE("cannot create package path\n");
return -1;
}
if (create_pkg_path(libdir, pkgname, PKG_LIB_POSTFIX, 0)) {
LOGE("cannot create package lib path\n");
return -1;
}
if (mkdir(pkgdir, 0751) < 0) {
LOGE("cannot create dir '%s': %s\n", pkgdir, strerror(errno));
return -errno;
}
if (chmod(pkgdir, 0751) < 0) {
LOGE("cannot chmod dir '%s': %s\n", pkgdir, strerror(errno));
unlink(pkgdir);
return -errno;
}
if (chown(pkgdir, uid, gid) < 0) {
LOGE("cannot chown dir '%s': %s\n", pkgdir, strerror(errno));
unlink(pkgdir);
return -errno;
}
if (mkdir(libdir, 0755) < 0) {
LOGE("cannot create dir '%s': %s\n", libdir, strerror(errno));
unlink(pkgdir);
return -errno;
}
if (chmod(libdir, 0755) < 0) {
LOGE("cannot chmod dir '%s': %s\n", libdir, strerror(errno));
unlink(libdir);
unlink(pkgdir);
return -errno;
}
if (chown(libdir, AID_SYSTEM, AID_SYSTEM) < 0) {
LOGE("cannot chown dir '%s': %s\n", libdir, strerror(errno));
unlink(libdir);
unlink(pkgdir);
return -errno;
}
return 0;
}
int install(const char *pkgname, uid_t uid, gid_t gid) { char pkgdir[PKG_PATH_MAX]; char libdir[PKG_PATH_MAX]; if ((uid < AID_SYSTEM) || (gid < AID_SYSTEM)) { LOGE("invalid uid/gid: %d %d\n", uid, gid); return -1; } if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, 0)) { LOGE("cannot create package path\n"); return -1; } if (create_pkg_path(libdir, pkgname, PKG_LIB_POSTFIX, 0)) { LOGE("cannot create package lib path\n"); return -1; } if (mkdir(pkgdir, 0751) < 0) { LOGE("cannot create dir '%s': %s\n", pkgdir, strerror(errno)); return -errno; } if (chmod(pkgdir, 0751) < 0) { LOGE("cannot chmod dir '%s': %s\n", pkgdir, strerror(errno)); unlink(pkgdir); return -errno; } if (chown(pkgdir, uid, gid) < 0) { LOGE("cannot chown dir '%s': %s\n", pkgdir, strerror(errno)); unlink(pkgdir); return -errno; } if (mkdir(libdir, 0755) < 0) { LOGE("cannot create dir '%s': %s\n", libdir, strerror(errno)); unlink(pkgdir); return -errno; } if (chmod(libdir, 0755) < 0) { LOGE("cannot chmod dir '%s': %s\n", libdir, strerror(errno)); unlink(libdir); unlink(pkgdir); return -errno; } if (chown(libdir, AID_SYSTEM, AID_SYSTEM) < 0) { LOGE("cannot chown dir '%s': %s\n", libdir, strerror(errno)); unlink(libdir); unlink(pkgdir); return -errno; } return 0; }
创建各种数据目录并对目录权限进行处理,这里只是创建了目录数据而已,APK文件的安装还是很复杂的,后面会分析。
相关文章推荐
- AS3 中的package(包)应用实例代码
- Orcle的package中访问其它Schema的表报错ORA-00942解决方法
- Lua中的模块(module)和包(package)详解
- Java基础教程之包(package)
- Python开发常用的一些开源Package分享
- 【编程基础】Java 中的Package和Import
- Python中的module,library,package之间的区别
- soft package install
- suse yast another process is accessing the package database
- Content Provider(以下简称为CP)的加载
- 绘制几何图形:使用android.graphics类
- Puppet错误及解决方法(三)
- CentOS安装Chrome
- glib 安装解决
- 建自己的网站
- ORACLE PL/SQL 包
- ORACLE PL/SQL REF参照类型
- VTUN(vtun.sourceforge.net)
- 动态拼接SQL语句
- java_MD5加密源码