Detected problems with API compatibility(visit g.co/dev/appcompat for more info)
最近手機升級了Android 9,在寫應用程式的時候進場會彈出一個彈框,如下在這裡插入圖片描述
嚇得我一身冷汗,在對應的網站上看了下資訊,原來是在android限制呼叫hide註解的api,注意這種現在並非原來的在sdk中簡單去掉hide註解的api,而是在虛擬機器層面做了限制。
本篇文章用於記錄整個調查過程。
首先彈出警告彈窗的位置在Activity.java中的performRestart函式中,有如下片段
7238 // This property is set for all non-user builds except final release 7239 boolean isApiWarningEnabled = SystemProperties.getInt("ro.art.hiddenapi.warning", 0) == 1; 7240 7241 if (isAppDebuggable || isApiWarningEnabled) { 7242 if (!mMainThread.mHiddenApiWarningShown && VMRuntime.getRuntime().hasUsedHiddenApi()) { 7243 // Only show the warning once per process. 7244 mMainThread.mHiddenApiWarningShown = true; 7245 7246 String appName = getApplicationInfo().loadLabel(getPackageManager()) 7247 .toString(); 7248 String warning = "Detected problems with API compatibility\n" 7249 + "(visit g.co/dev/appcompat for more info)"; 7250 if (isAppDebuggable) { 7251 new AlertDialog.Builder(this) 7252 .setTitle(appName) 7253 .setMessage(warning) 7254 .setPositiveButton(android.R.string.ok, null) 7255 .setCancelable(false) 7256 .show(); 7257 } else { 7258 Toast.makeText(this, appName + "\n" + warning, Toast.LENGTH_LONG).show(); 7259 } 7260 } 7261 }
- 7241行檢查是否需要檢查使用者呼叫了隱藏api(@hide註解的api),條件是應用開啟了除錯模式,或者
ro.art.hiddenapi.warning 屬性為1 - 經過上面一個步驟檢查還要經過7241的檢查,條件就是該應用啟動後還沒有彈出過呼叫隱藏api的警告,並且這期間呼叫了隱藏api,那麼就彈出警告。
- 警告有兩種方式,第一種是可調式的應用使用一個dialog彈出警告,否則使用toast彈出警告
經過上面的分析,我們就清楚了大致的流程
VMRuntime.getRuntime().hasUsedHiddenApi() 就是判斷應用有沒有呼叫過隱藏函式判斷的依據。
VMRuntime是art虛擬機器的執行時,對應art程式碼中的runtime.cc裡面的Runtime類,我們知道java和c++之間呼叫要使用jni做粘合劑,對應的jni程式碼在art/runtime/native/dalvik_system_VMRuntime.cc中,函式為
static jboolean VMRuntime_hasUsedHiddenApi(JNIEnv*, jobject) {
return Runtime::Current()->HasPendingHiddenApiWarning() ? JNI_TRUE : JNI_FALSE;
}
art Runtime是單例的,我們分析下HasPendingHiddenApiWarning函式
bool HasPendingHiddenApiWarning() const {
return pending_hidden_api_warning_;
}
就是讀取pending_hidden_api_warning_的變數值。
所以我們後面要關注的就是該值在哪裡被設定
void SetPendingHiddenApiWarning(bool value) {
pending_hidden_api_warning_ = value;
}
有三個地方呼叫該函式,其中兩個是設定值為false,說明用於清除該變數,我們不需要關心,那麼只有一個位置,在art/runtime/hidden_api.cc 檔案中,
我們需要關心的程式碼如下
template<typename T>
209 Action GetMemberActionImpl(T* member,
210 HiddenApiAccessFlags::ApiList api_list,
211 Action action,
212 AccessMethod access_method)
這是一個模板函式,其中有兩個實現
// Need to instantiate this.
template Action GetMemberActionImpl<ArtField>(ArtField* member,
HiddenApiAccessFlags::ApiList api_list,
Action action,
AccessMethod access_method);
template Action GetMemberActionImpl<ArtMethod>(ArtMethod* member,
HiddenApiAccessFlags::ApiList api_list,
Action action,
AccessMethod access_method);
在分析GetMemberActionImpl函式之前我們先來分析下哪裡呼叫了它,翻遍所有程式碼我們發現在art/runtime/hidden_api.h檔案中有呼叫
template<typename T>
inline Action GetMemberAction(T* member,
Thread* self,
std::function<bool(Thread*)> fn_caller_is_trusted,
AccessMethod access_method)
REQUIRES_SHARED(Locks::mutator_lock_) {
DCHECK(member != nullptr);
// Decode hidden API access flags.
// NB Multiple threads might try to access (and overwrite) these simultaneously,
// causing a race. We only do that if access has not been denied, so the race
// cannot change Java semantics. We should, however, decode the access flags
// once and use it throughout this function, otherwise we may get inconsistent
// results, e.g. print whitelist warnings (b/78327881).
HiddenApiAccessFlags::ApiList api_list = member->GetHiddenApiAccessFlags();
Action action = GetActionFromAccessFlags(member->GetHiddenApiAccessFlags());
if (action == kAllow) {
// Nothing to do.
return action;
}
// Member is hidden. Invoke `fn_caller_in_platform` and find the origin of the access.
// This can be *very* expensive. Save it for last.
if (fn_caller_is_trusted(self)) {
// Caller is trusted. Exit.
return kAllow;
}
// Member is hidden and caller is not in the platform.
return detail::GetMemberActionImpl(member, api_list, action, access_method);
}
函式裡面確認了兩個引數api_list 和action,api_list引數使用 member->GetHiddenApiAccessFlags() 函式獲取,其實該型別是一個列舉型別,程式碼使用該函式或者變數的型別,其中包括如下幾種
enum ApiList {
kWhitelist = 0, 白名單函式
kLightGreylist, 白灰名單
kDarkGreylist, 灰名單
kBlacklist, 黑名單
kNoList, 不在列表中
};
Action則代表遇到不同的名單列表執行的預設動作,也是一個列舉變數,使用
GetActionFromAccessFlags(member->GetHiddenApiAccessFlags()) 函式獲取
enum Action {
kAllow, //通過
kAllowButWarn, //通過但是警告
kAllowButWarnAndToast, //通過警告彈出toast
kDeny //拒絕
};
inline Action GetActionFromAccessFlags(HiddenApiAccessFlags::ApiList api_list) {
if (api_list == HiddenApiAccessFlags::kWhitelist) {
return kAllow; //白名單標誌預設動作是通過
}
//下面要根據EnforcementPolicy 決定如何執行預設動作,我們先不關係它
EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();
if (policy == EnforcementPolicy::kNoChecks) {
// Exit early. Nothing to enforce.
return kAllow;
}
// if policy is "just warn", always warn. We returned above for whitelist APIs.
if (policy == EnforcementPolicy::kJustWarn) {
return kAllowButWarn;
}
DCHECK(policy >= EnforcementPolicy::kDarkGreyAndBlackList);
// The logic below relies on equality of values in the enums EnforcementPolicy and
// HiddenApiAccessFlags::ApiList, and their ordering. Assertions are in hidden_api.cc.
if (static_cast<int>(policy) > static_cast<int>(api_list)) {
return api_list == HiddenApiAccessFlags::kDarkGreylist
? kAllowButWarnAndToast
: kAllowButWarn;
} else {
return kDeny;
}
}
看過app_list和action的含義後我們回來分析GetMemberActionImpl函式
208 template<typename T>
209 Action GetMemberActionImpl(T* member,
210 HiddenApiAccessFlags::ApiList api_list,
211 Action action,
212 AccessMethod access_method) {
213 DCHECK_NE(action, kAllow);
214
215 // Get the signature, we need it later.
216 MemberSignature member_signature(member);
217
218 Runtime* runtime = Runtime::Current();
219
220 // Check for an exemption first. Exempted APIs are treated as white list.
221 // We only do this if we're about to deny, or if the app is debuggable. This is because:
222 // - we only print a warning for light greylist violations for debuggable apps
223 // - for non-debuggable apps, there is no distinction between light grey & whitelisted APIs.
224 // - we want to avoid the overhead of checking for exemptions for light greylisted APIs whenever
225 // possible.
226 const bool shouldWarn = kLogAllAccesses || runtime->IsJavaDebuggable();
227 if (shouldWarn || action == kDeny) {
228 if (member_signature.IsExempted(runtime->GetHiddenApiExemptions())) {
//1 對於在豁免列表中的函式,直接放行
229 action = kAllow;
230 // Avoid re-examining the exemption list next time.
231 // Note this results in no warning for the member, which seems like what one would expect.
232 // Exemptions effectively adds new members to the whitelist.
233 MaybeWhitelistMember(runtime, member); //加入到白名單
234 return kAllow;
235 }
236
237 if (access_method != kNone) {
238 // Print a log message with information about this class member access.
239 // We do this if we're about to block access, or the app is debuggable.
240 member_signature.WarnAboutAccess(access_method, api_list); //2不能直接放行的列印log
241 }
242 }
243
244 if (kIsTargetBuild && !kIsTargetLinux) {
245 uint32_t eventLogSampleRate = runtime->GetHiddenApiEventLogSampleRate();
246 // Assert that RAND_MAX is big enough, to ensure sampling below works as expected.
247 static_assert(RAND_MAX >= 0xffff, "RAND_MAX too small");
248 if (eventLogSampleRate != 0 && //3 一些情況列印event log。還要控制速率
249 (static_cast<uint32_t>(std::rand()) & 0xffff) < eventLogSampleRate) {
250 member_signature.LogAccessToEventLog(access_method, action);
251 }
252 }
253
254 if (action == kDeny) { // action是拒絕的直接返回
255 // Block access
256 return action;
257 }
258
259 // Allow access to this member but print a warning.
260 DCHECK(action == kAllowButWarn || action == kAllowButWarnAndToast);
261
262 if (access_method != kNone) { //列印警告
263 // Depending on a runtime flag, we might move the member into whitelist and
264 // skip the warning the next time the member is accessed.
265 MaybeWhitelistMember(runtime, member);
266
267 // If this action requires a UI warning, set the appropriate flag.
268 if (shouldWarn &&
269 (action == kAllowButWarnAndToast || runtime->ShouldAlwaysSetHiddenApiWarningFlag())) {
270 runtime->SetPendingHiddenApiWarning(true);
271 }
272 }
273
274 return action;
275 }
從上面的程式碼數可以看到,GetMemberActionImpl函式主要是用於對不同action進行log列印加白等工作。
由此可見,最終要的函式還是GetActionFromAccessFlags 函式。
到這裡我們的問題大致就清楚了
整個框架是根據方法中的一個flags去獲取對應的執行動作,其中還有兩點一點,flags是從哪設定的,hidden_api_policy_又是怎麼設定的。