背景
ANR分析需要结合各个层面大量的数据,诸如堆栈信息、Android log等。可能是app出了问题,也可能是framwork出了问题,甚至是内核出了问题。那么理解anr的机制,才能分析好具体的ANR问题。
ANR就是应用无响应,超过一定时间,由system server进程弹窗提示。
ANR分类:1.Service(前台超时20s) 2. Broadcast(前台超时10s) 3. ContentProvider(前台超时10s) 4.InputDispatcing(超时5s)
ANR过程:埋炸弹—>拆炸弹/引爆炸弹
ANR原理
Sevice ANR原理:
Service启动过程中:1、AMS收到启动请求,由ActiveServices埋炸弹。2、AMS资源处理完成通知app,app在service的onCreate()之后,请求AMS拆除炸弹,由ActiveServices拆除炸弹。3、如果启动超时,将由ActiveServices引爆炸弹
1、埋炸弹
/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
final ActivityManagerService mAm;
//启动流程,经过层层函数到这儿
private void realStartServiceLocked(ServiceRecord r, ProcessRecord app,IApplicationThread thread, int pid, UidRecord uidRecord, boolean execInFg,boolean enqueueOomAdj) throws RemoteException {
bumpServiceExecutingLocked(r, execInFg, "create",OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */);
//通知app走onCreate()
thread.scheduleCreateService(r, r.serviceInfo,null,app.mState.getReportedProcState());
}
private boolean bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why, @OomAdjReason int oomAdjReason) {
scheduleServiceTimeoutLocked(r.app);
}
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
//炸弹开始倒计时
mAm.mHandler.sendMessageDelayed(msg, proc.mServices.shouldExecServicesFg()? mAm.mConstants.SERVICE_TIMEOUT :mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT);
}
2、拆炸弹
/frameworks/base/core/java/android/app/ActivityThread.java
//app接到通知
private class ApplicationThread extends IApplicationThread.Stub {
public final void scheduleCreateService(IBinder token,ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
sendMessage(H.CREATE_SERVICE, s);
}
}
class H extends Handler {//主线程
public void handleMessage(Message msg) {
switch (msg.what) {
case CREATE_SERVICE:
handleCreateService((CreateServiceData)msg.obj);
break;
}
}
}
private void handleCreateService(CreateServiceData data) {
service.onCreate();
ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
}
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
final ActiveServices mServices;
public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {
mServices.serviceDoneExecutingLocked((ServiceRecord) token, type, startId, res, false);
}
/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,boolean finishing, boolean enqueueOomAdj, @OomAdjReason int oomAdjReason) {
//拆除炸弹
mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
}
3、引爆炸弹
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
final MainHandler mHandler;
final ActiveServices mServices;
final class MainHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case SERVICE_TIMEOUT_MSG: {
mServices.serviceTimeout((ProcessRecord) msg.obj);
} break;
case SERVICE_FOREGROUND_TIMEOUT_MSG: {
mServices.serviceForegroundTimeout((ServiceRecord) msg.obj);
} break;
}
}
}
/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
void serviceTimeout(ProcessRecord proc) {
mAm.mAnrHelper.appNotResponding(proc, timeoutRecord);
}
Broadcast ANR原理:
发送广播的过程:1、AMS收到app请求,由BroadcastQueueImpl埋炸弹。2、AMS完成资源处理,通知app回调receiver的onReceive(),之后app请求AMS拆除炸弹 3、如果超时,由BroadcastQueueImpl引爆炸弹
注意:串行广播才有ANR,并行广播没有。广播ANR细分为2种:1、broadcast的ANR,主要计算receiver数量的总时长。 2、BroadcastReceiver的ANR,主要计算单个receiver的时长。
1、埋炸弹
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
//app请求到这儿
public final int broadcastIntentWithFeature(IApplicationThread caller, .....) {
return broadcastIntentLocked(callerApp,...........);
}
final int broadcastIntentLocked(ProcessRecord callerApp,......){
final int res = broadcastIntentLockedTraced(callerApp,........);
}
final int broadcastIntentLockedTraced(ProcessRecord callerApp,....){
BroadcastQueue queue = broadcastQueueForIntent(intent);
queue.enqueueBroadcastLocked(r);
}
/frameworks/base/services/core/java/com/android/server/am/BroadcastQueueImpl.java
final BroadcastHandler mHandler;
boolean mBroadcastsScheduled = false;//标志-有广播正在处理
public void enqueueBroadcastLocked(BroadcastRecord r) {
enqueueOrderedBroadcastLocked(r);
scheduleBroadcastsLocked();
}
public void scheduleBroadcastsLocked() {
if (mBroadcastsScheduled) {//当前广播正在处理,拒绝下一个广播
return;
}
mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
mBroadcastsScheduled = true;
}
private final class BroadcastHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case BROADCAST_INTENT_MSG: {
processNextBroadcast(true);
} break;
case BROADCAST_TIMEOUT_MSG: {
broadcastTimeoutLocked(true);
} break;
}
}
}
private void processNextBroadcast(boolean fromMsg) {
processNextBroadcastLocked(fromMsg, false);
}
public void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
BroadcastRecord r;
if (fromMsg) {
mBroadcastsScheduled = false;
}
................................//并行广播分发省略,不存在ANR
//处理串行广播
do {
r = mDispatcher.getNextBroadcastLocked(now);
//广播分发,总时间超时,也会ANR。
if (mService.mProcessesReady && !r.timeoutExempt && r.dispatchTime > 0) {
if ((numReceivers > 0) && (now > r.dispatchTime + (2 * mConstants.TIMEOUT * numReceivers))) {
broadcastTimeoutLocked(false); // forcibly finish this broadcast,此处也会弹ANR,省略
}
}
} while (r == null);
//准备分发给receiver了
int recIdx = r.nextReceiver++;
r.receiverTime = SystemClock.uptimeMillis();//更新 每个receiver分发 开始时间,用于判断单个receiver是否超时。
if (recIdx == 0) {//记录 第一个receiver 分发时间,控制分发总时间的ANR。埋了一个“总时长炸弹”
r.dispatchTime = r.receiverTime;
}
//埋了“单个receiver炸弹”
setBroadcastTimeoutLocked(timeoutTime);
//分发receiver
deliverToRegisteredReceiverLocked(r, filter, r.ordered, recIdx);
}
//埋炸弹
final void setBroadcastTimeoutLocked(long timeoutTime) {
if (! mPendingBroadcastTimeoutMessage) {//上一个receiver的炸弹,倒计时没结束,当前receiver暂不埋炸弹。
Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
mHandler.sendMessageAtTime(msg, timeoutTime);
mPendingBroadcastTimeoutMessage = true;
}
}
private void deliverToRegisteredReceiverLocked(BroadcastRecord r,BroadcastFilter filter, boolean ordered, int index) {
performReceiveLocked(r,........);
}
//通知app走reciever的onRecieve()
public void performReceiveLocked(BroadcastRecord r,){
final IApplicationThread thread = app.getThread();
thread.scheduleRegisteredReceiver(......);
}
2、拆炸弹
/frameworks/base/core/java/android/app/ActivityThread.java
private class ApplicationThread extends IApplicationThread.Stub {
public void scheduleRegisteredReceiver(IIntentReceiver receiver, ....){
receiver.performReceive(intent, resultCode, dataStr, extras, ordered, sticky, sendingUser);
}
}
/frameworks/base/core/java/android/app/LoadedApk.java
static final class ReceiverDispatcher {
final static class InnerReceiver extends IIntentReceiver.Stub {
public void performReceive(){
final LoadedApk.ReceiverDispatcher rd;
rd.performReceive(intent, resultCode, data, extras,ordered, sticky, assumeDelivered, sendingUser,sendingUid, sendingPackage);
}
}
public void performReceive(Intent intent, int resultCode, String data,Bundle extras, boolean ordered, boolean sticky, boolean assumeDelivered,int sendingUser, int sendingUid, String sendingPackage) {
final Args args = new Args(intent, resultCode, data, extras, ordered,sticky, assumeDelivered, sendingUser, sendingUid, sendingPackage);
if (intent == null || !mActivityThread.post(args.getRunnable())) {//调用receiver的onReceive()
IActivityManager mgr = ActivityManager.getService();
args.sendFinished(mgr);
}
}
final class Args extends BroadcastReceiver.PendingResult {
public final Runnable getRunnable() {
final BroadcastReceiver receiver = mReceiver;
receiver.onReceive(mContext, intent);
}
}
}
/frameworks/base/core/java/android/content/BroadcastReceiver.java
public static class PendingResult {
public void sendFinished(IActivityManager am) {
am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras,mAbortBroadcast, mFlags);
}
}
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public void finishReceiver(IBinder caller, int resultCode, String resultData,Bundle resultExtras, boolean resultAbort, int flags) {
queue.finishReceiverLocked(callerApp, resultCode,resultData, resultExtras, resultAbort, true);
}
/frameworks/base/services/core/java/com/android/server/am/BroadcastQueueImpl.java
public boolean finishReceiverLocked(ProcessRecord app, int resultCode,String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
return finishReceiverLocked(r, resultCode, resultData, resultExtras, resultAbort, waitForServices);
}
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
processNextBroadcastLocked(/* fromMsg= */ false, /* skipOomAdj= */ true);
}
public void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
do {//处理串行广播
//分发最后一个receiver时,拆掉 总时长炸弹。
if (r.receivers == null || r.nextReceiver >= numReceivers || r.resultAbort || forceReceive) {
cancelBroadcastTimeoutLocked();
}
} while (r == null);
}
//这里拆除单个receiver炸弹。这里的代码真的太骚了!!!我找了好久!!!
final void broadcastTimeoutLocked(boolean fromMsg) {
if (fromMsg) {//从Handler倒计时满了过来的,才设置下面的标志位,表示允许埋 单个receiver炸弹
mPendingBroadcastTimeoutMessage = false;
}
if (fromMsg) {
long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
if (timeoutTime > now) {//下一个receiver开始分发了,r.receiverTime被更新,并且下一个receiver还没超时。那本次也没超时
setBroadcastTimeoutLocked(timeoutTime);//这里才开始埋下一个receiver的炸弹
return;//本次分发没超时,直接return了。惊不惊喜意不意外!
}
}
}
3、引爆炸弹
/frameworks/base/services/core/java/com/android/server/am/BroadcastQueueImpl.java
final void broadcastTimeoutLocked(boolean fromMsg) {
//前面的代码判断超时,会走到这一步
mService.appNotResponding(app, timeoutRecord);
}
ContentProvider ANR原理:
首先ContentProvider的启动过程,包含在app的启动过程中。AMS亲自埋炸弹。注意不会弹ANR弹窗,会直接杀死app。
1、埋炸弹
/frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
}
private void attach(boolean system, long startSeq) {
final IActivityManager mgr = ActivityManager.getService();
mgr.attachApplication(mAppThread, startSeq);
}
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public final void attachApplication(IApplicationThread thread, long startSeq) {
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
}
private boolean attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) {
List<ProviderInfo> providers = normalMode ? mCpHelper.generateApplicationProvidersLocked(app) : null;
//埋炸弹
if (providers != null && mCpHelper.checkAppInLaunchingProvidersLocked(app)) {
Message msg = mHandler.obtainMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG);
msg.obj = app;
mHandler.sendMessageDelayed(msg, ContentResolver.CONTENT_PROVIDER_PUBLISH_TIMEOUT_MILLIS);
}
//通知app处理ContentProvider流程
thread.bindApplication(processName, appInfo, providerList,........);
}
2、拆炸弹
/frameworks/base/core/java/android/app/ActivityThread.java
private class ApplicationThread extends IApplicationThread.Stub {
public final void bindApplication(String processName, ApplicationInfo appInfo, ProviderInfoList providerList, ...) {
sendMessage(H.BIND_APPLICATION, data);
}
}
class H extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case BIND_APPLICATION:
handleBindApplication(data);
break;
}
}
}
private void handleBindApplication(AppBindData data) {
installContentProviders(app, data.providers);
mInstrumentation.callApplicationOnCreate(app);
}
private void installContentProviders(Context context, List<ProviderInfo> providers) {
//创建ContentProvider
ContentProviderHolder cph = installProvider(context, null, cpi,false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
results.add(cph);
ActivityManager.getService().publishContentProviders(getApplicationThread(), results);
}
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public final void publishContentProviders(IApplicationThread caller,List<ContentProviderHolder> providers) {
mCpHelper.publishContentProviders(caller, providers);
}
/frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java
void publishContentProviders(IApplicationThread caller, List<ContentProviderHolder> providers) {
//拆除炸弹
mService.mHandler.removeMessages( ActivityManagerService.CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, r);
}
3、引爆炸弹
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
final class MainHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG: {
mCpHelper.processContentProviderPublishTimedOutLocked(app);
} break;
}
}
}
/frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java
void processContentProviderPublishTimedOutLocked(ProcessRecord app) {
cleanupAppInLaunchingProvidersLocked(app, true);
mService.mProcessList.removeProcessLocked(app,.....);
}
/frameworks/base/services/core/java/com/android/server/am/ProcessList.java
boolean removeProcessLocked(ProcessRecord app,.....) {
//直接干掉app
app.killLocked(reason, reasonCode, subReason, true, async);
}
InputDispatching ANR原理:
EventHub负责监听/dev/input产生的input事件,InputReader负责从EventHub读取事件,并将事件分发给InputDispatcher,InputDispatcher分发给当前获取到焦点的Window。ANR在InputDispatcher的流程里面。
埋炸弹
/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// 超时时间
const std::chrono::duration DEFAULT_INPUT_DISPATCHING_TIMEOUT = std::chrono::milliseconds( android::os::IInputConstants::UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS *HwTimeoutMultiplier());
status_t InputDispatcher::start() {
mThread = std::make_unique<InputThread>("InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });
}
void InputDispatcher::dispatchOnce() {
dispatchOnceInnerLocked(&nextWakeupTime);
//检查ANR
const nsecs_t nextAnrCheck = processAnrsLocked();
}
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
//mInboundQueue取出一个Event
mPendingEvent = mInboundQueue.front();
switch (mPendingEvent->type) {
case EventEntry::Type::KEY: {//按键
done = dispatchKeyLocked(currentTime, keyEntry, &dropReason, nextWakeupTime);
break;
}
case EventEntry::Type::MOTION: {//手势
done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
}
}
}
// 分发按键
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
// 找到focusedWindow,这里 会埋下 noFocusedWindow超时时间
sp<WindowInfoHandle> focusedWindow =findFocusedWindowTargetLocked(currentTime, *entry, nextWakeupTime,injectionResult);
dispatchEventLocked(currentTime, entry, inputTargets);
}
// 分发手势
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,DropReason* dropReason, nsecs_t* nextWakeupTime) {
if (isPointerEvent) {
}else{// 找到focusedWindow,这里 会埋下 noFocusedWindow超时时间
sp<WindowInfoHandle> focusedWindow = findFocusedWindowTargetLocked(currentTime, *entry, nextWakeupTime, injectionResult);
}
dispatchEventLocked(currentTime, entry, inputTargets);
}
sp<WindowInfoHandle> InputDispatcher::findFocusedWindowTargetLocked(){
// 有focused的app,但没有focused的window
if (focusedWindowHandle == nullptr && focusedApplicationHandle != nullptr) {
if (!mNoFocusedWindowTimeoutTime.has_value()) {
std::chrono::nanoseconds timeout = focusedApplicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
//埋炸弹--noFocusedWindow类型ANR
mNoFocusedWindowTimeoutTime = currentTime + timeout.count();
return nullptr;
}
}
// 有focusedWindow取消ANR计时
resetNoFocusedWindowTimeoutLocked();
}
// 不管是按键还是手势,都到分发事件函数
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,std::shared_ptr<EventEntry> eventEntry, const std::vector<InputTarget>& inputTargets) {
for (const InputTarget& inputTarget : inputTargets) {
prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);
}
}
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,const std::shared_ptr<Connection>& connection,std::shared_ptr<EventEntry> eventEntry,const InputTarget& inputTarget) {
enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}
void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,const std::shared_ptr<Connection>& connection,std::shared_ptr<EventEntry> eventEntry,const InputTarget& inputTarget) {
//将dispatchEntry放入connection->outboundQueue
enqueueDispatchEntryLocked(connection, eventEntry, inputTarget, InputTarget::Flags::DISPATCH_AS_HOVER_EXIT);
startDispatchCycleLocked(currentTime, connection);
}
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const std::shared_ptr<Connection>& connection) {
while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
DispatchEntry* dispatchEntry = connection->outboundQueue.front();
const std::chrono::nanoseconds timeout = getDispatchingTimeoutLocked(connection);
//设置超时时间
dispatchEntry->timeoutTime = currentTime + timeout.count();
switch (eventEntry.type) {
case EventEntry::Type::KEY: {//将按键事件分发给Java层
status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,...);
break;
}
case EventEntry::Type::MOTION: {//将手势事件分发给Java层
status = publishMotionEvent(*connection, *dispatchEntry);
break;
}
}
//为了表示正在分发中,event重新入队wait queue.
connection->outboundQueue.erase(std::remove(connection->outboundQueue.begin(),connection->outboundQueue.end(),dispatchEntry));
connection->waitQueue.push_back(dispatchEntry);
//埋炸弹--分发超时ANR
mAnrTracker.insert(dispatchEntry->timeoutTime, connection->inputChannel->getConnectionToken());
}
}
std::chrono::nanoseconds InputDispatcher::getDispatchingTimeoutLocked(const std::shared_ptr<Connection>& connection) {
return DEFAULT_INPUT_DISPATCHING_TIMEOUT;
}
status_t InputDispatcher::publishMotionEvent(Connection& connection,DispatchEntry& dispatchEntry) const {
return connection.inputPublisher.publishMotionEvent(dispatchEntry.seq,...);
}
/frameworks/native/libs/input/android/os/IInputConstants.aidl
const int UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS = 5000; // 5 seconds
拆炸弹
引爆炸弹
/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
nsecs_t InputDispatcher::processAnrsLocked() {
nsecs_t nextAnrCheck = LLONG_MAX; // 下一次检查anr的时间
// 检查 是否在等待 聚焦窗口出现。如果超时 报ANR
if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
if (currentTime >= *mNoFocusedWindowTimeoutTime) {
//场景1:noFocusedWindow的ANR
processNoFocusedWindowAnrLocked();
}
}
// 检查分发类型 ANRs 是否超时
nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());
if (currentTime < nextAnrCheck) {
return nextAnrCheck;
}
// 分发 超时了
std::shared_ptr<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());
//删除记录的超时
mAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());
// 场景2:分发超时ANR
onAnrLocked(connection);
}
void InputDispatcher::processNoFocusedWindowAnrLocked() {
// 没有焦点app 或 焦点app不是我们等的那个
if (focusedApplication == nullptr || focusedApplication->getApplicationToken() != mAwaitedFocusedApplication->getApplicationToken()) {
return;//直接返回,因为焦点app已经变更
}
// 再次检查焦点app,是否焦点window
const sp<WindowInfoHandle>& focusedWindowHandle = getFocusedWindowHandleLocked(mAwaitedApplicationDisplayId);
if (focusedWindowHandle != nullptr) {
return; // 有focused window了. 无需触发ANR了.
}
onAnrLocked(mAwaitedFocusedApplication);
}
void InputDispatcher::onAnrLocked(const std::shared_ptr<Connection>& connection) {
// 之前将正在分发的event放到了waitQueue,现在它空了,那就不弹ANR了
if (connection->waitQueue.empty()) {
return;
}
processConnectionUnresponsiveLocked(*connection, std::move(reason));
}
void InputDispatcher::processConnectionUnresponsiveLocked(const Connection& connection,std::string reason) {
sendWindowUnresponsiveCommandLocked(connectionToken, pid, std::move(reason));
}
ANR分析步骤
源码
参考版本:14.0.0
http://aospxref.com/