1. 程式人生 > >Detected problems with API compatibility(visit g.co/dev/appcompat for more info)

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         }
  1. 7241行檢查是否需要檢查使用者呼叫了隱藏api(@hide註解的api),條件是應用開啟了除錯模式,或者
    ro.art.hiddenapi.warning 屬性為1
  2. 經過上面一個步驟檢查還要經過7241的檢查,條件就是該應用啟動後還沒有彈出過呼叫隱藏api的警告,並且這期間呼叫了隱藏api,那麼就彈出警告。
  3. 警告有兩種方式,第一種是可調式的應用使用一個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_又是怎麼設定的。