关于 Isming | 码农明明桑

程序员,安徽人,现居上海。擅长Android开发,对Python和Web前端有所了解。

RSS 地址: https://isming.me/index.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

Isming | 码农明明桑 RSS 预览

Android源码分析:广播接收器注册与发送广播流程解析

2024-10-17 19:40:16

广播,顾名思义就是把一个信息传播出去,在Android中也提供了广播和广播接收器BroadcastReceiver,用来监听特定的事件和发送特定的消息。不过广播分为全局广播和本地广播,本地广播是在Android Jetpack库中所提供,其实现也是基于Handler和消息循环机制,并且这个类Android官方也不推荐使用了。我们这里就来看看Android全局的这个广播。

应用开发者可以自己发送特定的广播,而更多场景则是接收系统发送的广播。注册广播接收器有在AndroidManifest文件中声明和使用代码注册两种方式,在应用的target sdk大于等于Android 8.0(Api Version 26)之后,系统会限制在清单文件中注册。通过清单方式注册的广播,代码中没有注册逻辑,只有PMS中读取它的逻辑,我们这里不进行分析。

注册广播接收器

首先是注册广播接收器,一般注册一个广播接收器的代码如下:

1
2
3
val br: BroadcastReceiver = MyBroadcastReceiver()
val filter = IntentFilter(ACTION_CHARGING)
activity.registerReceiver(br, filter)

使用上面的代码就能注册一个广播接收器,当手机开始充电就会收到通知,会去执行MyBroadcastReceiveronReceive方法。

那我们就从这个registerReceiver来时往里面看,因为Activity是Context的子类,这个注册的方法的实现则是在ContextImpl当中,其中最终调用的方法为registerReceiverInternal,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,  
        IntentFilter filter, String broadcastPermission,  
        Handler scheduler, Context context, int flags) {  
    IIntentReceiver rd = null;  
    if (receiver != null) {  
        if (mPackageInfo != null && context != null) {  
            if (scheduler == null) {  
                scheduler = mMainThread.getHandler();  
            }  
            rd = mPackageInfo.getReceiverDispatcher(  
                receiver, context, scheduler,  
                mMainThread.getInstrumentation(), true);  
        } else {  
           ...
        }  
    }  
    try {  
        ActivityThread thread = ActivityThread.currentActivityThread();  
        Instrumentation instrumentation = thread.getInstrumentation();  
        if (instrumentation.isInstrumenting()  
                && ((flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {  
            flags = flags | Context.RECEIVER_EXPORTED;  
        }  
        final Intent intent = ActivityManager.getService().registerReceiverWithFeature(  
                mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),  
                AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,  
                flags);  
        if (intent != null) {  
            intent.setExtrasClassLoader(getClassLoader());  
			intent.prepareToEnterProcess(
				ActivityThread.isProtectedBroadcast(intent),  
                getAttributionSource());  
        }  
        return intent;  
    } catch (RemoteException e) {  
        ...
    }  
}

我们在注册广播的时候只传了两个参数,但是实际上它还可以传不少的参数,这里userId就是注册的用户id,会被自动 填充成当前进程的用户Id,broadcastPermission表示这个广播的权限,也就是说需要有该权限的应用发送的广播,这个接收者才能接收到。scheduler就是一个Handler,默认不传,在第8行可以看到,会拿当前进程的主线程的Handlerflag是广播的参数,这里比较重要的就是RECEIVER_NOT_EXPORTED,添加了它则广播不会公开暴露,其他应用发送的消息不会被接收。

在第10行,这里创建了一个广播的分发器,在24行,通过AMS去注册广播接收器,只有我们的broadcast会用到contentprovider或者有sticky广播的时候,30行才会执行到,这里跳过。

获取广播分发器

首先来看如何获取广播分发器,这块的代码在LoadedApk.java中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,  
        Context context, Handler handler,  
        Instrumentation instrumentation, boolean registered) {  
    synchronized (mReceivers) {  
        LoadedApk.ReceiverDispatcher rd = null;  
        ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;  
        if (registered) {  
            map = mReceivers.get(context);  
            if (map != null) {  
                rd = map.get(r);  
            }  
        }  
        if (rd == null) {  
            rd = new ReceiverDispatcher(r, context, handler,  
                    instrumentation, registered);  
            if (registered) {  
                if (map == null) {  
                    map = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();  
                    mReceivers.put(context, map);  
                }  
                map.put(r, rd);  
            }  
        } else {  
            rd.validate(context, handler);  
        }  
        rd.mForgotten = false;  
        return rd.getIIntentReceiver();  
    }  
}

先来说一下mReceivers,它的结构为ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>>,也就是嵌套了两层的ArrayMap,外层是以Context为key,内层以Receiver为key,实际存储的为ReceiverDispatcherReceiverDispatcher内部所放的IIntentReceiver比较重要,也就是我们这个方法所返回的值,它实际是IIntentReceiver.Stub,也就是它的Binder实体类。

这段代码的逻辑也比较清晰,就是根据ContextReceiver到map中去查找看是否之前注册过,如果注册过就已经有这个Dispatcher了,如果没有就创建一个,并且放到map中去,最后返回binder对象出去。

AMS注册广播接收器

在AMS注册的代码很长,我们这里主要研究正常的普通广播注册,关于黏性广播,instantApp的广播,以及广播是否导出等方面都省略不予研究。以下为我们关注的核心代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,  
        String callerFeatureId, String receiverId, IIntentReceiver receiver,  
        IntentFilter filter, String permission, int userId, int flags) {
        ...
        synchronized(this) {
	        ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());  
			if (rl == null) {  
			    rl = new ReceiverList(this, callerApp, callingPid, callingUid,  
		            userId, receiver);  
			    if (rl.app != null) {  
			        final int totalReceiversForApp = rl.app.mReceivers.numberOfReceivers();  
			        if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {  
			            throw new IllegalStateException("Too many receivers, total of "  
	                    + totalReceiversForApp + ", registered for pid: "  
	                    + rl.pid + ", callerPackage: " + callerPackage);  
			        }  
			        rl.app.mReceivers.addReceiver(rl);  
			    } else {  
			        try {  
			            receiver.asBinder().linkToDeath(rl, 0);  
			        } catch (RemoteException e) {  
			            return sticky;  
			        }  
			        rl.linkedToDeath = true;  
				}  
			    mRegisteredReceivers.put(receiver.asBinder(), rl);  
			} else {
			 // 处理userId, uid,pid 等不同的错误
			}

			BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,  
		        receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,  
		        exported);  
			if (rl.containsFilter(filter)) {  
			} else {  
		    rl.add(bf);  
		    mReceiverResolver.addFilter(getPackageManagerInternal().snapshot(), bf);  
			}
        }
        ...        
}

在前面ContextImpl中调用AMS注册Reciever的地方,我们传的就是Receiver的Binder实体,这里拿到的是binder引用。在代码中我们可以看到,首先会以我们传过来的receiver的binder对象为key,到mRegisterReceivers当中去获取ReceiverList,这里我们就知道receiver在System_server中是怎样存储的了。如果AMS当中没有,会去创建一个ReceiverList并放置到这个map当中去,如果存在则不需要做什么事情。但是这一步只是放置了Receiver,而我们的Receiver对应的关心的IntentFilter还没使用,这里就需要继续看31行的代码了。在这里这是使用了我们传过来的IntentFilter创建了一个BroadcastFilter对象,并且把它放到了ReceiverList当中,同时还放到了mReceiverResolver当中,这个对象它不是一个Map而是一个IntentResolver,其中会存储我们的BroadcastFilter,具体这里先不分析了。 BroadcastReceiver 存放结构

到这里我们就看完了广播接收器的注册,在App进程和System_Server中分别将其存储,具体两边的数据结构如上图所示。这里可以继续看看发送广播的流程了。

发送广播

一般我们发送广播会调用如下的代码:

1
2
3
4
5
Intent().also { intent -> 
	intent.setAction("com.example.broadcast.MY_NOTIFICATION") 
	intent.putExtra("data", "Nothing to see here, move along.")  
	activity.sendBroadcast(intent)  
}

我们通过设置Action来匹配对应的广播接收器,通过设置Data或者Extra,这样广播接收器中可以接收到对应的数据,最后调用sendBroadcast来发送。而sendBroadcast的实现也是在ContextImpl中,源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override  
public void sendBroadcast(Intent intent) {  
    warnIfCallingFromSystemProcess();  
    String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());  
    try {  
        intent.prepareToLeaveProcess(this);  
        ActivityManager.getService().broadcastIntentWithFeature(  
                mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,  
                null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,  
                null, AppOpsManager.OP_NONE, null, false, false, getUserId());  
    } catch (RemoteException e) {  
        throw e.rethrowFromSystemServer();  
    }  
}

这里代码比较简单,就是直接调用AMS的broadcastIntentWithFeature来发送广播。

AMS发送广播

这里我们可以直接看AMS中的broadcastIntentWithFeature的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override  
public final int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,  
        Intent intent, String resolvedType, IIntentReceiver resultTo,  
        int resultCode, String resultData, Bundle resultExtras,  
        String[] requiredPermissions, String[] excludedPermissions,  
        String[] excludedPackages, int appOp, Bundle bOptions,  
        boolean serialized, boolean sticky, int userId) {  
    enforceNotIsolatedCaller("broadcastIntent");  
    synchronized(this) {  
        intent = verifyBroadcastLocked(intent);  
  
        final ProcessRecord callerApp = getRecordForAppLOSP(caller);  
        final int callingPid = Binder.getCallingPid();  
        final int callingUid = Binder.getCallingUid();  
  
        final long origId = Binder.clearCallingIdentity();  
        try {  
            return broadcastIntentLocked(callerApp,  
                    callerApp != null ? callerApp.info.packageName : null, callingFeatureId,  
                    intent, resolvedType, resultTo, resultCode, resultData, resultExtras,  
                    requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,  
                    serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId);  
        } finally {  
            Binder.restoreCallingIdentity(origId);  
        }  
    }  
}

第10行代码,主要验证Intent,比如检查它的Flag,检查它是否传文件描述符之类的,里面的代码比较简单清晰,这里不单独看了。后面则是获取调用者的进程,uid,pid之类的,最后调用broadcastIntentLocked,这个方法的代码巨多,接近1000行代码,我们同样忽略sticky的广播,也忽略顺序广播,然后来一点一点的看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//ActivityManagerService.java 
//final int broadcastIntentLocked(...)
intent = new Intent(intent);
intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
if (!mProcessesReady && (intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0) {  
    intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);  
}
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,  
        ALLOW_NON_FULL, "broadcast", callerPackage);
final String action = intent.getAction();

首先这里的代码是对Intent做一下封装,并且如果系统还在启动,不允许启动应用进程,以及获取当前的用户ID,大部分情况下,我们只需要考虑一个用户的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (action != null) {
	...
	switch (action) {
		...
		case Intent.ACTION_PACKAGE_DATA_CLEARED:
		{  
		    Uri data = intent.getData();  
		    String ssp;  
		    if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {  
		        mAtmInternal.onPackageDataCleared(ssp, userId);  
		    }  
		    break;  
		}  
		case Intent.ACTION_TIMEZONE_CHANGED:  
			mHandler.sendEmptyMessage(UPDATE_TIME_ZONE);  
		    break;
		    ...
	}
}

对于一些系统的广播事件,除了要发送广播给应用之外,在AMS中,还会根据其广播,来调用相关的服务或者执行相关的逻辑,也会在这里调用其代码。这里我罗列了清除应用数据和时区变化两个广播,其他的感兴趣的可以自行阅读相关代码。

1
2
3
4
5
6
int[] users;  
if (userId == UserHandle.USER_ALL) {  
    users = mUserController.getStartedUserArray();  
} else {  
    users = new int[] {userId};  
}

以上代码为根据前面拿到的userId,来决定广播要发送给所有人还是仅仅发送给当前用户,并且把userId保存到users数组当中。

获取广播接收者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
List receivers = null;  
List<BroadcastFilter> registeredReceivers = null;  
if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {  
    receivers = collectReceiverComponents(  
            intent, resolvedType, callingUid, users, broadcastAllowList);  
}  
if (intent.getComponent() == null) {  
    final PackageDataSnapshot snapshot = getPackageManagerInternal().snapshot();  
    if (userId == UserHandle.USER_ALL && callingUid == SHELL_UID) {  
        ...
    } else {  
        registeredReceivers = mReceiverResolver.queryIntent(snapshot, intent,  
                resolvedType, false /*defaultOnly*/, userId);  
    }  
}

以上为获取我们注册的所有的接收器的代码,其中FLAG_RECEIVER_REGISTERED_ONLY意味着仅仅接收注册过的广播,前面在判断当前系统还未启动完成的时候有添加这个FLAG,其他情况一般不会有这个Flag,这里我们按照没有这个flag处理。那也就会执行第4行的代码。另外下面还有从mReceiverResolver从获取注册的接收器的代码,因为大部分情况不是从shell中执行的,因此也忽略了其代码。

首先看collectReceiverComponents的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
private List<ResolveInfo> collectReceiverComponents(Intent intent, String resolvedType,  
        int callingUid, int[] users, int[] broadcastAllowList) {  
    int pmFlags = STOCK_PM_FLAGS | MATCH_DEBUG_TRIAGED_MISSING;  
  
    List<ResolveInfo> receivers = null;  
    HashSet<ComponentName> singleUserReceivers = null;  
    boolean scannedFirstReceivers = false;  
    for (int user : users) {  
        List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers(  
                intent, resolvedType, pmFlags, callingUid, user, true /* forSend */);  //通过PMS,根据intent和uid读取Manifest中注册的接收器
        if (user != UserHandle.USER_SYSTEM && newReceivers != null) {  
            for (int i = 0; i < newReceivers.size(); i++) {  
                ResolveInfo ri = newReceivers.get(i);  
                //如果调用不是系统用户,移除只允许系统用户接收的接收器
                if ((ri.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {  
                    newReceivers.remove(i);  
                    i--;  
                }  
            }  
        }  
        // 把别名替换成真实的接收器  
        if (newReceivers != null) {  
            for (int i = newReceivers.size() - 1; i >= 0; i--) {  
                final ResolveInfo ri = newReceivers.get(i);  
                final Resolution<ResolveInfo> resolution =  
                        mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType,  
                                pmFlags, user, callingUid, true /* forSend */);  
                if (resolution == null) {  
                    // 未找到对应的接收器,删除这个记录 
                    newReceivers.remove(i);  
                    continue;  
                }  
                if (resolution.isAlias()) {  
	                //找到对应的真实的接收器,就把别名的记录替换成真实的目标
                    newReceivers.set(i, resolution.getTarget());  
                }  
            }  
        }  
        if (newReceivers != null && newReceivers.size() == 0) {  
            newReceivers = null;  
        }  
  
        if (receivers == null) {  
            receivers = newReceivers;  
        } else if (newReceivers != null) {  
            if (!scannedFirstReceivers) {  
	            //查找单用户记录的接收器,并且保存
                scannedFirstReceivers = true;  
                for (int i = 0; i < receivers.size(); i++) {  
                    ResolveInfo ri = receivers.get(i);  
                    if ((ri.activityInfo.flags&ActivityInfo.FLAG_SINGLE_USER) != 0) {  
                        ComponentName cn = new ComponentName(  
                                ri.activityInfo.packageName, ri.activityInfo.name);  
                        if (singleUserReceivers == null) {  
                            singleUserReceivers = new HashSet<ComponentName>();  
                        }  
                        singleUserReceivers.add(cn);  
                    }  
                }  
            }  
            for (int i = 0; i < newReceivers.size(); i++) {  
                ResolveInfo ri = newReceivers.get(i);  
                if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {  
                    ComponentName cn = new ComponentName(  
                            ri.activityInfo.packageName, ri.activityInfo.name);  
                    if (singleUserReceivers == null) {  
                        singleUserReceivers = new HashSet<ComponentName>();  
                    }  
                    if (!singleUserReceivers.contains(cn)) {  
	                    //对于单用户的接收器,只存一次到返回结果中
                        singleUserReceivers.add(cn);  
                        receivers.add(ri);  
                    }  
                } else {  
                    receivers.add(ri);  
                }  
            }  
        }  
    }
    ...
    return receivers;  
}

以上就根据信息通过PMS获取所有通过Manifest静态注册的广播接收器,对其有一些处理,详见上面的注释。

对于我们在代码中动态注册的接收器,则需要看mReceiverResolver.queryIntent的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
protected final List<R> queryIntent(@NonNull PackageDataSnapshot snapshot, Intent intent,  
        String resolvedType, boolean defaultOnly, @UserIdInt int userId, long customFlags) {  
    ArrayList<R> finalList = new ArrayList<R>();  
    F[] firstTypeCut = null;  
    F[] secondTypeCut = null;  
    F[] thirdTypeCut = null;  
    F[] schemeCut = null;  
  
    if (resolvedType == null && scheme == null && intent.getAction() != null) {  
        firstTypeCut = mActionToFilter.get(intent.getAction());  
    }  
  
    FastImmutableArraySet<String> categories = getFastIntentCategories(intent);  
    Computer computer = (Computer) snapshot;  
    if (firstTypeCut != null) {  
        buildResolveList(computer, intent, categories, debug, defaultOnly, resolvedType,  
                scheme, firstTypeCut, finalList, userId, customFlags);  
    }  
    sortResults(finalList);  //按照IntentFilter的priority优先级降序排序
    return finalList;  
}

以上代码中,这个mActionToFilter就是我们前面注册广播时候,将BroadcastFilter添加进去的一个ArrayMap,这里会根据Action去其中取出所有的BroadcastFilter,之后调用buildResolveList将其中的不符合本次广播接收要求的广播接收器给过滤掉,最后按照IntentFilter的优先级降序排列。

到这里我们就有两个列表receivers存放Manifest静态注册的将要本次广播接收者,和registeredReceivers通过代码手动注册的广播接收者。

广播入队列

首先来看通过代码注册的接收器不为空,并且不是有序广播的情况,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int NR = registeredReceivers != null ? registeredReceivers.size() : 0;  
if (!ordered && NR > 0) {  
    ...
    final BroadcastQueue queue = broadcastQueueForIntent(intent);  
    BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,  
            callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,  
            requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,  
            registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,  
            sticky, false, userId, allowBackgroundActivityStarts,  
            backgroundActivityStartsToken, timeoutExempt);  
    ...
    final boolean replaced = replacePending  
            && (queue.replaceParallelBroadcastLocked(r) != null);  
    if (!replaced) {  
        queue.enqueueParallelBroadcastLocked(r);  
        queue.scheduleBroadcastsLocked();  
    }  
    registeredReceivers = null;  
    NR = 0;  
}

在这里,第4行会首先根据intent的flag获取对应的BroadcastQueue,这里有四个Queue,不看其代码了,不过逻辑如下:

  1. 如果有FLAG_RECEIVER_OFFLOAD_FOREGROUND 标记,则使用mFgOffloadBroadcastQueue
  2. 如果当前开启了offloadQueue,也就是mEnableOffloadQueue,并且有FLAG_RECEIVER_OFFLOAD标记,则使用mBgOffloadBroadcastQueue
  3. 如果有FLAG_RECEIVER_FOREGROUND,也就是前台时候才接收广播,则使用mFgBroadcastQueue
  4. 如果没有上述标记,则使用mBgBroadcastQueue。 拿到queue之后,会创建一条BroadcastRecord,其中会记录传入的参数,intent,以及接收的registeredReceivers,调用queue的入队方法,最后把registeredReceivers设置为null,计数也清零。具体入队的代码,我们随后再看,这里先看其他情况下的广播入队代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
int ir = 0;  
if (receivers != null) {  
    String skipPackages[] = null;  
    //对于添加应用,删除应用数据之类的广播,不希望变化的应用能够接收到对应的广播
    //这里设置忽略它们
    if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())  
            || Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())  
            || Intent.ACTION_PACKAGE_DATA_CLEARED.equals(intent.getAction())) {  
        Uri data = intent.getData();  
        if (data != null) {  
            String pkgName = data.getSchemeSpecificPart();  
            if (pkgName != null) {  
                skipPackages = new String[] { pkgName };  
            }  
        }  
    } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction())) {  
        skipPackages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);  
    }  
    if (skipPackages != null && (skipPackages.length > 0)) {  
	    //如果Manifest注册的广播接收器的包名和skip的一样,那就移除它们
        for (String skipPackage : skipPackages) {  
            if (skipPackage != null) {  
                int NT = receivers.size();  
                for (int it=0; it<NT; it++) {  
                    ResolveInfo curt = (ResolveInfo)receivers.get(it);  
                    if (curt.activityInfo.packageName.equals(skipPackage)) {  
                        receivers.remove(it);  
                        it--;  
                        NT--;  
                    }  
                }  
            }  
        }  
    }  
  
    int NT = receivers != null ? receivers.size() : 0;  
    int it = 0;  
    ResolveInfo curt = null;  
    BroadcastFilter curr = null;  
    while (it < NT && ir < NR) {  
        if (curt == null) {  
            curt = (ResolveInfo)receivers.get(it);  
        }  
        if (curr == null) {  
            curr = registeredReceivers.get(ir);  
        }  
        if (curr.getPriority() >= curt.priority) {  
            //如果动态注册的广播优先级比静态注册的等级高,就把它添加到静态注册的前面。
            receivers.add(it, curr);  
            ir++;  
            curr = null;  
            it++;  
            NT++;  
        } else {  
            // 如果动态注册的广播优先级没有静态注册的等级高,那就移动静态注册的游标,下一轮在执行相关的判断。
            it++;  
            curt = null;  
        }  
    }  
}
while (ir < NR) {  //如果registeredReceivers中的元素没有全部放到receivers里面,就一个一个的遍历并放进去。
    if (receivers == null) {  
        receivers = new ArrayList();  
    }  
    receivers.add(registeredReceivers.get(ir));  
    ir++;  
}

以上的代码所做的事情就是首先移除静态注册的广播当中需要忽略的广播接收器,随后将静态注册和动态注册的广播接收器,按照优先级合并到同一个列表当中,当然如果动态注册的前面已经入队过了,这里实际上是不会在合并的。关于合并的代码,就是经典的两列表合并的算法,具体请看代码和注释。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
if ((receivers != null && receivers.size() > 0)  
        || resultTo != null) {  
    BroadcastQueue queue = broadcastQueueForIntent(intent);  
    BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,  
            callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,  
            requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,  
            receivers, resultTo, resultCode, resultData, resultExtras,  
            ordered, sticky, false, userId, allowBackgroundActivityStarts,  
            backgroundActivityStartsToken, timeoutExempt);  
  
    final BroadcastRecord oldRecord =  
            replacePending ? queue.replaceOrderedBroadcastLocked(r) : null;  
    if (oldRecord != null) {  
        if (oldRecord.resultTo != null) {  
            final BroadcastQueue oldQueue = broadcastQueueForIntent(oldRecord.intent);  
            try {  
                oldRecord.mIsReceiverAppRunning = true;  
                oldQueue.performReceiveLocked(oldRecord.callerApp, oldRecord.resultTo,  
                        oldRecord.intent,  
                        Activity.RESULT_CANCELED, null, null,  
                        false, false, oldRecord.userId, oldRecord.callingUid, callingUid,  
                        SystemClock.uptimeMillis() - oldRecord.enqueueTime, 0);  
            } catch (RemoteException e) {  
  
            }  
        }  
    } else {  
        queue.enqueueOrderedBroadcastLocked(r);  
        queue.scheduleBroadcastsLocked();  
    }  
}else {
	//对于无人关心的广播,也做一下记录
	if (intent.getComponent() == null && intent.getPackage() == null  
        && (intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) { 
	    addBroadcastStatLocked(intent.getAction(), callerPackage, 0, 0, 0);  
	}
}

以上的代码,跟前面入队的代码也差不多,不过这里如果采用的方法是enqueueOrderedBroadcastLocked,并且多了关于已经发送的广播的替换的逻辑,这里我们先不关注。如果receivers为空,并且符合条件的隐式广播,系统也会对其进行记录,具体,我们这里也不进行分析了。

BroadcastQueue 入队

我们知道前面入队的时候有两个方法,分别是enqueueParallelBroadcastLockedenqueueOrderedBroadcastLocked,我们先来分析前者。

1
2
3
4
5
6
7
public void enqueueParallelBroadcastLocked(BroadcastRecord r) {  
    r.enqueueClockTime = System.currentTimeMillis();  
    r.enqueueTime = SystemClock.uptimeMillis();  
    r.enqueueRealTime = SystemClock.elapsedRealtime();  
    mParallelBroadcasts.add(r);  
    enqueueBroadcastHelper(r);  
}

这里就是将BroadcastRecord放到mParallelBroadcasts列表中,随后执行enqueueBroadcastHelper,我们先看继续看一下enqueueOrderedBroadcastLocked方法。

1
2
3
4
5
6
7
public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {  
    r.enqueueClockTime = System.currentTimeMillis();  
    r.enqueueTime = SystemClock.uptimeMillis();  
    r.enqueueRealTime = SystemClock.elapsedRealtime();  
    mDispatcher.enqueueOrderedBroadcastLocked(r);  
    enqueueBroadcastHelper(r);  
}

这里跟上面很类似,差别是这里把BroadcastRecord入队了mDispatcher,对于普通广播,其内部是把这个记录放到了mOrderedBroadcasts列表。 而enqueueBroadcastHelper方法仅仅用于trace,我们这里不需要关注。

到了这里,我们把广播放到对应的列表了,但是广播还是没有分发出去。

AMS端广播的分发

以上是代码入了BroadcastQueu,接下来就可以看看队列中如何处理它了。首先需要注意一下,记录在入队的同时还调用了BroadcastQueuescheduleBroadcastsLock方法,代码如下:

1
2
3
4
5
6
7
public void scheduleBroadcastsLocked() {  
    if (mBroadcastsScheduled) {  
        return;  
    }  
    mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));  
    mBroadcastsScheduled = true;  
}

这里使用了Handler发送了一条BROADCAST_INTENT_MSG消息,我们可以去看一下BroadcastHandlerhandleMessage方法。其中在处理这个消息的时候调用了processNextBroadcast方法,我们可以直接去看其实现:

1
2
3
4
5
private void processNextBroadcast(boolean fromMsg) {  
    synchronized (mService) {  
        processNextBroadcastLocked(fromMsg, false);  
    }  
}

这里开启了同步块调用了processNextBroadcastLocked方法,这个方法依然很长,其中涉及到广播的权限判断,对于静态注册的广播,可能还涉及到对应进程的启动等。

动态广播的分发

动态注册的无序广播相对比较简单,这里我们仅仅看一下其中无序广播的分发处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
if (fromMsg) {  
    mBroadcastsScheduled = false;  //通过handleMessage过来,把flag设置为false
}
while (mParallelBroadcasts.size() > 0) {  
    r = mParallelBroadcasts.remove(0);  
    r.dispatchTime = SystemClock.uptimeMillis();  
    r.dispatchRealTime = SystemClock.elapsedRealtime();  
    r.dispatchClockTime = System.currentTimeMillis();  
    r.mIsReceiverAppRunning = true;  
    final int N = r.receivers.size();  
    
    for (int i=0; i<N; i++) {  
        Object target = r.receivers.get(i);  
        
        deliverToRegisteredReceiverLocked(r,  
                (BroadcastFilter) target, false, i);  //分发
    }  
    addBroadcastToHistoryLocked(r);  //把广播添加的历史记录中
}


这里就是遍历`ParallelBroadcasts`中的每一条`BroadcastRecord`,其中会再分别遍历每一个`BroadcastFilter`,调用`deliverToRegisteredReceiverLocked`来分发广播
```java
private void deliverToRegisteredReceiverLocked(BroadcastRecord r,  
        BroadcastFilter filter, boolean ordered, int index) {  
    boolean skip = false;  
    ...

    if (filter.requiredPermission != null) {  
        int perm = mService.checkComponentPermission(filter.requiredPermission,  
                r.callingPid, r.callingUid, -1, true);  
        if (perm != PackageManager.PERMISSION_GRANTED) {  
            skip = true;  
        } else {  
            final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);  
            if (opCode != AppOpsManager.OP_NONE  
                    && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,  
                    r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")  
                    != AppOpsManager.MODE_ALLOWED) {  
                skip = true;  
            }  
        }  
    }  
    ...
    if (skip) {  
        r.delivery[index] = BroadcastRecord.DELIVERY_SKIPPED;  
        return;  
    }  
    
    r.delivery[index] = BroadcastRecord.DELIVERY_DELIVERED;  
    ...
    try {  
        
        if (filter.receiverList.app != null && filter.receiverList.app.isInFullBackup()) {  
            if (ordered) {  
                skipReceiverLocked(r);  
            }  
        } else {  
            r.receiverTime = SystemClock.uptimeMillis();  
            maybeAddAllowBackgroundActivityStartsToken(filter.receiverList.app, r);  
            maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);  
            maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);  
            performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,  
                    new Intent(r.intent), r.resultCode, r.resultData,  
                    r.resultExtras, r.ordered, r.initialSticky, r.userId,  
                    filter.receiverList.uid, r.callingUid,  
                    r.dispatchTime - r.enqueueTime,  
                    r.receiverTime - r.dispatchTime);  
            if (filter.receiverList.app != null  
                    && r.allowBackgroundActivityStarts && !r.ordered) {  
                postActivityStartTokenRemoval(filter.receiverList.app, r);  
            }  
        }  
        if (ordered) {  
            r.state = BroadcastRecord.CALL_DONE_RECEIVE;  
        }  
    } catch (RemoteException e) {  
        ...
        if (ordered) {  
            r.receiver = null;  
            r.curFilter = null;  
            filter.receiverList.curBroadcast = null;  
        }  
    }  
}

在这个方法中有大段的代码是判断是否需要跳过当前这个广播,我这里仅仅保留了几句权限检查的代码。对于跳过的记录会将其BroadcastRecorddelivery[index]值设置为DELIVERY_SKIPPED, 而成功分发的会设置为DELIVERY_DELIVERED。对于有序广播的分发我们这里也不予分析,直接看无序广播的分发,在分发之前会尝试给对应的接收进程添加后台启动Activity的权限,这个会在分发完成之后恢复原状,调用的是maybeAddAllowBackgroundActivityStartsToken,就不具体分析了。

之后会调用performReceiveLocked去进行真正的分发,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,  
        Intent intent, int resultCode, String data, Bundle extras,  
        boolean ordered, boolean sticky, int sendingUser,  
        int receiverUid, int callingUid, long dispatchDelay,  
        long receiveDelay) throws RemoteException {  
    if (app != null) {  
        final IApplicationThread thread = app.getThread();  
        if (thread != null) {  
            try {  
                thread.scheduleRegisteredReceiver(receiver, intent, resultCode,  
                        data, extras, ordered, sticky, sendingUser,  
                        app.mState.getReportedProcState());  
            } catch (RemoteException ex) {  
                ...
                throw ex;  
            }  
        } else {  
            ...
            throw new RemoteException("app.thread must not be null");  
        }  
    } else {  
        receiver.performReceive(intent, resultCode, data, extras, ordered,  
                sticky, sendingUser);  
    }  
    ...
}

在执行分发的代码中,如果我们的ProcessRecord不为空,并且ApplicationThread也存在的情况下,会调用它的scheduleRegisterReceiver方法。如果进程记录为空,则会直接使用IIntentReceiverperformReceiver方法。我们在App中动态注册的情况,ProcessRecord一定是不为空的,我们也以这种情况继续向下分析。

动态注册广播分发App进程逻辑

1
2
3
4
5
6
7
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,  
        int resultCode, String dataStr, Bundle extras, boolean ordered,  
        boolean sticky, int sendingUser, int processState) throws RemoteException {  
    updateProcessState(processState, false);  
    receiver.performReceive(intent, resultCode, dataStr, extras, ordered,  
            sticky, sendingUser);  
}

在应用进程中,首先也只是根据AMS传过来的processState更新一下进程的状态,随后还是调用了IIntentReceiverperformReceive方法,performReceiveLoadedApk当中,为内部类InnerReceiver的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void performReceive(Intent intent, int resultCode, String data,  
        Bundle extras, boolean ordered, boolean sticky, int sendingUser) {  
    final LoadedApk.ReceiverDispatcher rd;  
    if (intent == null) {  
        rd = null;  
    } else {  
        rd = mDispatcher.get();  //获取ReceiverDispatcher
    }  
    if (rd != null) {  
        rd.performReceive(intent, resultCode, data, extras,  
                ordered, sticky, sendingUser);  
    } else {  
        IActivityManager mgr = ActivityManager.getService();  
        try {  
            if (extras != null) {  
                extras.setAllowFds(false);  
            }  
            mgr.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());  
        } catch (RemoteException e) {  
            throw e.rethrowFromSystemServer();  
        }  
    }  
}

在应用进程中,首先会获取ReceiverDisptcher,这个一般不会为空。但是系统代码比较严谨,也考虑了,不存在的情况会调用AMS的finishReceiver完成整个流程。

对于存在的情况,会调用ReceiverDispatcherperformReceive方法继续分发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void performReceive(Intent intent, int resultCode, String data,  
        Bundle extras, boolean ordered, boolean sticky, int sendingUser) {  
    final Args args = new Args(intent, resultCode, data, extras, ordered,  
            sticky, sendingUser);  
    ..
    if (intent == null || !mActivityThread.post(args.getRunnable())) {  
        if (mRegistered && ordered) {  
            IActivityManager mgr = ActivityManager.getService();  
            ..
            args.sendFinished(mgr);  
        }  
    }  
}

这里的代码有点绕,不过也还比较清晰,首先是创建了一个Args对象,之后根据java的语法,如果intent不为空的时候会执行如下代码:

1
mActivityThread.post(args.getRunnable())

当这个执行失败的时候,才会看情况执行8行到第10行的代码。而这个Runnable就是应用端真正分发的逻辑,其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public final Runnable getRunnable() {  
    return () -> {  
        final BroadcastReceiver receiver = mReceiver;  
        final boolean ordered = mOrdered;  

  
        final IActivityManager mgr = ActivityManager.getService();  
        final Intent intent = mCurIntent;  
  
        mCurIntent = null;  
        mDispatched = true;  
        mRunCalled = true;  
        if (receiver == null || intent == null || mForgotten) {  
           ...
            return;  
        }  
        try {  
            ClassLoader cl = mReceiver.getClass().getClassLoader();  
            intent.setExtrasClassLoader(cl);  
            intent.prepareToEnterProcess(ActivityThread.isProtectedBroadcast(intent),  
                    mContext.getAttributionSource());  
            setExtrasClassLoader(cl);  
            receiver.setPendingResult(this);  
            receiver.onReceive(mContext, intent);  
        } catch (Exception e) {  
            if (mRegistered && ordered) {    
                sendFinished(mgr);  
            }  
            if (mInstrumentation == null ||  
                    !mInstrumentation.onException(mReceiver, e)) {  
                throw new RuntimeException(  
                        "Error receiving broadcast " + intent  
                                + " in " + mReceiver, e);  
            }  
        }  
  
        if (receiver.getPendingResult() != null) {  
            finish();  
        }  
    };  
}

这里的receiver就是我们注册时候的那个BroadcastReceiver,这里将当前的Args对象作为它的PendingResult,在这里调用了它的onReceive方法 ,最后看pendingResult是否为空,不为空则调用PendingResultfinish()方法。当我们在onReceive中编写代码的时候,如果调用了goAsync的话,那这里的PendingResult就会为空。

另外就是我们这个Runnable是使用的mActivityThread的post方法投递出去的,它是一个Handler对象,它是在注册广播接收器的时候指定的,默认是应用的主线程Handler,也就是说广播的执行会在主线程。

但是即使是我们使用goAsync的话,处理完成之后也是需要手动调用finish的,我们后面在来看相关的逻辑。

静态广播的发送

在前面分析的BroadcastQueueprocessNextBroadcastLocked方法中,我们只分析了动态广播的发送,这里再看一下静态广播的发送,首先仍然是看processNextBroadcastLocked中的相关源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BroadcastRecord r;
do {
	r = mDispatcher.getNextBroadcastLocked(now);
	if (r == null) {
		...
		return;
	}
	...

} while(r === null);
...
if (app != null && app.getThread() != null && !app.isKilled()) {
	try {
		app.addPackage(info.activityInfo.packageName,  
        info.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);  
		maybeAddAllowBackgroundActivityStartsToken(app, r);  
		r.mIsReceiverAppRunning = true;  
		processCurBroadcastLocked(r, app);  
		return;
	} catch(RemoteException e) {
		...
	}
}
...

在第3行,会从mDispatcher中拿BroadcastRecord的记录,我们之前在AMS端入队的代码,对于静态注册的广播和有序广播都是放在mDispatcher当中的,这里拿到动态注册的有序广播也会从这里拿,它的后续逻辑跟前面分析的是一样的,这里不再看了。对于静态注册的广播,在调用后续的方法之前,需要先获取对应进程的ProcessRecord,和ApplicationThread,并且进行广播权限的检查,进程是否存活检查这些在我们11行的位置,都省略不看了。如果App进程存活则会走到我们12行的部分,否则会去创建对应的进程,创建完进程会再去分发广播。

动态注册的广播,会传一个IIntentReceiver的Binder到AMS,而静态注册的广播,我们跟着第18行代码processCurBroadcastLocked方法进去一览究竟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private final void processCurBroadcastLocked(BroadcastRecord r,  
        ProcessRecord app) throws RemoteException {   
    final IApplicationThread thread = app.getThread();  
    ...
    r.receiver = thread.asBinder();  
    r.curApp = app;  
    final ProcessReceiverRecord prr = app.mReceivers;  
    prr.addCurReceiver(r);  
    app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);  
    ...
    r.intent.setComponent(r.curComponent);  
  
    boolean started = false;  
    try {   
        mService.notifyPackageUse(r.intent.getComponent().getPackageName(),  
                                  PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);  
        thread.scheduleReceiver(new Intent(r.intent), r.curReceiver,  
                mService.compatibilityInfoForPackage(r.curReceiver.applicationInfo),  
                r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,  
                app.mState.getReportedProcState());    
        started = true;  
    } finally {  
        if (!started) {   
            r.receiver = null;  
            r.curApp = null;  
            prr.removeCurReceiver(r);  
        }  
    }  

}

在这个方法中,把App的ProcessRecord放到了BroadcastRecord当中,并且把ApplicationThread设置为receiver,最后是调用了ApplicationThreadscheduleReceiver,从而通过binder调用App进程。

静态注册广播分发App进程逻辑

通过Binder调用,在App的ApplicationThread代码中,调用的是如下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final void scheduleReceiver(Intent intent, ActivityInfo info,  
        CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,  
        boolean sync, int sendingUser, int processState) {  
    updateProcessState(processState, false);  
    ReceiverData r = new ReceiverData(intent, resultCode, data, extras,  
            sync, false, mAppThread.asBinder(), sendingUser);  
    r.info = info;  
    r.compatInfo = compatInfo;  
    sendMessage(H.RECEIVER, r);  
}

这里是创建了一个ReceiverData把AMS传过来数据包裹其中,并且通过消息发出去,之后会调用ActivityThreadhandleReceiver方法, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void handleReceiver(ReceiverData data) {  
    String component = data.intent.getComponent().getClassName();  
  
    LoadedApk packageInfo = getPackageInfoNoCheck(  
            data.info.applicationInfo, data.compatInfo);  
  
    IActivityManager mgr = ActivityManager.getService();  
  
    Application app;  
    BroadcastReceiver receiver;  
    ContextImpl context;  
    try {  
        app = packageInfo.makeApplicationInner(false, mInstrumentation);  
        context = (ContextImpl) app.getBaseContext();  
        if (data.info.splitName != null) {  
            context = (ContextImpl) context.createContextForSplit(data.info.splitName);  
        }  
        if (data.info.attributionTags != null && data.info.attributionTags.length > 0) {  
            final String attributionTag = data.info.attributionTags[0];  
            context = (ContextImpl) context.createAttributionContext(attributionTag);  
        }  
        java.lang.ClassLoader cl = context.getClassLoader();  
        data.intent.setExtrasClassLoader(cl);  
        data.intent.prepareToEnterProcess(  
                isProtectedComponent(data.info) || isProtectedBroadcast(data.intent),  
                context.getAttributionSource());  
        data.setExtrasClassLoader(cl);  
        receiver = packageInfo.getAppFactory()  
                .instantiateReceiver(cl, data.info.name, data.intent);  
    } catch (Exception e) {  
        data.sendFinished(mgr);  
       ...
    }  
  
    try {  
        
        sCurrentBroadcastIntent.set(data.intent);  
        receiver.setPendingResult(data);  
        receiver.onReceive(context.getReceiverRestrictedContext(),  
                data.intent);  
    } catch (Exception e) {  
        data.sendFinished(mgr);  
    } finally {  
        sCurrentBroadcastIntent.set(null);  
    }  
  
    if (receiver.getPendingResult() != null) {  
        data.finish();  
    }  
}

这个代码中主要有两个try-catch的代码块,分别是两个主要的功能区。因为静态注册的广播,我们的广播接收器是没有构建的,AMS传过来的只是广播的类名,因此,第一块代码的功能就是创建广播接收器对象。第二块代码则是去调用广播接收器的onReceive方法,从而传递广播。另外这里会调用PendingResultfinish去执行广播处理完成之后的逻辑,以及告知AMS,不过这里的PendingResult就是前面创建的ReceiverData

完成广播的发送

在分析前面的动态注册广播分发和静态注册广播分发的时候,最终在App进程它们都有一个Data,静态为ReceiverData, 动态为Args,他们都继承了PendingResult,最终都会调用PendingResultfinish方法来完成后面的收尾工作,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public final void finish() {  
    if (mType == TYPE_COMPONENT) {  
        final IActivityManager mgr = ActivityManager.getService();  
        if (QueuedWork.hasPendingWork()) {  
            QueuedWork.queue(new Runnable() {  
                @Override public void run() {  
                    sendFinished(mgr);  
                }  
            }, false);  
        } else {  
            sendFinished(mgr);  
        }  
    } else if (mOrderedHint && mType != TYPE_UNREGISTERED) {  
        final IActivityManager mgr = ActivityManager.getService();  
        sendFinished(mgr);  
    }  
}

这里的QueuedWork主要用于运行SharedPreferences写入数据到磁盘,当然这个如果其中有未运行的task则会添加一个Task到其中来运行sendFinished,这样做的目的是为了保证如果当前除了广播接收器没有别的界面或者Service运行的时候,AMS不会杀掉当前的进程。否则会直接运行sendFinished方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void sendFinished(IActivityManager am) {  
    synchronized (this) {  
        if (mFinished) {  
            throw new IllegalStateException("Broadcast already finished");  
        }  
        mFinished = true;  
        try {  
            if (mResultExtras != null) {  
                mResultExtras.setAllowFds(false);  
            }  
            if (mOrderedHint) {  
                am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras,  
                        mAbortBroadcast, mFlags);  
            } else {  
                am.finishReceiver(mToken, 0, null, null, false, mFlags);  
            }  
        } catch (RemoteException ex) {  
        }  
    }  
}

这里就是调用AMS的finishReceiver方法,来告诉AMS广播接收的处理已经执行完了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void finishReceiver(IBinder who, int resultCode, String resultData,  
        Bundle resultExtras, boolean resultAbort, int flags) {  
    if (resultExtras != null && resultExtras.hasFileDescriptors()) {  
        throw new IllegalArgumentException("File descriptors passed in Bundle");  
    }  
  
    final long origId = Binder.clearCallingIdentity();  
    try {  
        boolean doNext = false;  
        BroadcastRecord r;  
        BroadcastQueue queue;  
  
        synchronized(this) {  
            if (isOnFgOffloadQueue(flags)) {  
                queue = mFgOffloadBroadcastQueue;  
            } else if (isOnBgOffloadQueue(flags)) {  
                queue = mBgOffloadBroadcastQueue;  
            } else {  
                queue = (flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0  
                        ? mFgBroadcastQueue : mBgBroadcastQueue;  
            }  
  
            r = queue.getMatchingOrderedReceiver(who);  
            if (r != null) {  
                doNext = r.queue.finishReceiverLocked(r, resultCode,  
                    resultData, resultExtras, resultAbort, true);  
            }  
            if (doNext) { 
            }  
            trimApplicationsLocked(false, OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER);  
        }  
  
    } finally {  
        Binder.restoreCallingIdentity(origId);  
    }  
}

相关的逻辑从13行开始,首先仍然是根据广播的flag找到之前的BroadcastQueue,之后根据IBinder找到发送的这一条BroadcastRecord,调用Queue的finishReceiverLocked方法。根据它的返回值,再去处理队列中的下一个广播记录。最后的trimApplicationsLocked里面会视情况来决定是否停止App进程,我们这里就不进行分析了。

processNextBroadcastLocaked前面已经分析过了,这里只需要来看finishReceiverLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,  
        String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {  
    final int state = r.state;  
    final ActivityInfo receiver = r.curReceiver;  
    final long finishTime = SystemClock.uptimeMillis();  
    final long elapsed = finishTime - r.receiverTime;  
    r.state = BroadcastRecord.IDLE;  
    final int curIndex = r.nextReceiver - 1;  
    if (curIndex >= 0 && curIndex < r.receivers.size() && r.curApp != null) {  
        final Object curReceiver = r.receivers.get(curIndex);  
        
    }  
    ...
  
    r.receiver = null;  
    r.intent.setComponent(null);  
    if (r.curApp != null && r.curApp.mReceivers.hasCurReceiver(r)) {  
        r.curApp.mReceivers.removeCurReceiver(r);  
        mService.enqueueOomAdjTargetLocked(r.curApp);  
    }  
    if (r.curFilter != null) {  
        r.curFilter.receiverList.curBroadcast = null;  
    }  
    r.curFilter = null;  
    r.curReceiver = null;  
    r.curApp = null;  
    mPendingBroadcast = null;  
  
    r.resultCode = resultCode;  
    r.resultData = resultData;  
    r.resultExtras = resultExtras;  
    ....
    r.curComponent = null;  
  
    return state == BroadcastRecord.APP_RECEIVE  
            || state == BroadcastRecord.CALL_DONE_RECEIVE;  
}

在这里,我们最关注的代码就是17行开是的代码,从mReceivers列表中移除BroadcastRecord,并且把ReceiverListcurBroadcast设置为空,并且其他几个参数也设置为空,这样才算完成了广播的分发和处理。

总结

以上就是广播接收器的注册,以及动态、静态广播分发的分析了。关于取消注册是跟注册相关的过程,理解了注册的逻辑,取消注册也可以很快的搞清楚。关于sticky的广播,限于篇幅先不分析了。而有序广播,它在AMS端其实和静态注册的广播是差不多,不过它在调用App进程的时候是有差别的。另外关于权限相关的逻辑,以后在权限代码的分析中可以再进行关注。

看完评论一下吧

更优雅的RSS使用指南

2024-10-14 21:32:37

最近因为Follow的爆火,RSS的内容也跟着一起火了一把。笔者最近也优化了一下自己博客的RSS输出,在这里写一下博客如何更加 优雅的输出RSS,以及在订阅RSS的时候如何更好的发现RSS源。

RSS2.0 与 ATOM

RSS是一种消息来源格式,用于方便的将一个站点的内容以一个指定的格式输出,方便订阅者聚合多个站点的内容。

目前RSS的版本为2.0,而我们大家在使用博客输出RSS文件的时候,除了常用的RSS2.0格式,目前还有一个ATOM格式,其目前的版本为1.0。Atom发展的动机为了解决RSS2.0的问题,它解决了如下问题(来源WikiPedia):

  • RSS 2.0可能包含文本或经过编码的HTML内容,同时却没有提供明确的区分办法;相比之下,Atom则提供了明确的标签(也就是typed)。
  • RSS 2.0的description标签可以包含全文或摘要(尽管该标签的英文含义为描述或摘要)。Atom则分别提供了summary和content标签,用以区分摘要和内容,同时Atom允许在summary中添加非文本内容。
  • RSS 2.0存在多种非标准形式的应用,而Atom具有统一的标准,这便于内容的聚合和发现。
  • Atom有符合XML标准的命名空间,RSS 2.0却没有。
  • Atom通过XML内置的xml:base标签来指示相对地址URI,RSS2.0则无相应的机制区分相对地址和绝对地址。
  • Atom通过XML内置的xml:lang,而RSS采用自己的language标签。
  • Atom强制为每个条目设定唯一的ID,这将便于内容的跟踪和更新。
  • Atom 1.0允许条目单独成为文档,RSS 2.0则只支持完整的种子文档,这可能产生不必要的复杂性和带宽消耗。
  • Atom按照RFC3339标准表示时间 ,而RSS2.0中没有指定统一的时间格式。
  • Atom 1.0具有在IANA注册了的MIME类型,而RSS 2.0所使用的application/rss+xml并未注册。
  • Atom 1.0标准包括一个XML schema,RSS 2.0却没有。
  • Atom是IETF组织标准化程序下的一个开放的发展中标准,RSS 2.0则不属于任何标准化组织,而且它不是开放版权的。

相比之下ATOM协议是有更多的有点,如果你RSS生成程序已经支持了Atom那肯定是优先使用Atom。不过现在基本上99%以上的Rss订阅器或者工具对于两者都有很好的支持,因此如果你现在已经使用了RSS2.0也没必要替换成Atom了。

RSS的自动发现

对于提供Rss订阅的网站,最好的方式是提供相应的连接或者使用Rss图标,告诉访客当前网站的Rss地址。

除了这样之外,我们还应该在网站的源码中添加RSS地址,这样对于一些浏览器插件或者订阅软件可以通过我们的网站页面自动发现RSS订阅地址。

对于RSS2.0的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />

对于ATOM的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/atom+xml" href="atom.xml" title="Site title" />

如果你同时提供了ATOM和RSS2.0两种订阅文件,可以上面两行代码都添加。当然现在一些博客程序的模板文件中已经添加了上面的代码,检查一下即可。

RSS输出的优化

因为我的博客是以RSS2.0格式输出的订阅文件,因此这里我就按照我的优化内容来介绍一下输出相关的优化,对于ATtom可以参考其规范文档。

首先区分介绍和全文的输出。对于只输出描述的网站只需要设置描述部分即可,对于输出了全部的博客,还是建议同时输出描述和全文的。 而RSS2.0不支持输出全文,我们可以用一下的标记来输出全文:

1
<content:encoded>全文内容</content:encoded>

其中的文章html,最好做一下转码。 (以上代码加的有问题,有的RSS识别失败,暂时回退了,有时间换Atom)

其次可以补充一下网站的内容的元数据,比如作者的信息,网站的标题简介等等。

对于文章,也可以在输出的时候输出相关的元数据,如标题,作者,标签等。标签支持设置多个,可以用如下的标记:

1
<category domain="{{ .Permalink }}">{{ .LinkTitle }}</category>

另外在我设置的过程,发现rss是提供了一个comments标记的,设置这个标记后,如果RSS阅读器对此支持,理论上可以直接从RSS阅读器点击跳转到文章的评论页面。

最后,我们可能想要检测要多少通过RSS点击跳转到我们博客的访问量,这个时候可以在输出的链接上面加上特定的参数,这样在我们的统计平台上面就可以看到有多少用户从这里打开页面的,我所加的参数如下:

?utm_source=rss

订阅RSS

目前最流行的订阅RSS的方式要属于Follow了,这里也推荐使用。

除了Follow之外,我还自建了一个FreshRss来订阅一些内容,这个的使用要更早于Follow的出现。现在还不能抛弃它的原因是Follow目前不支持移动端,我使用Android的手机,在移动推荐使用FeedMe来浏览FreshRss的订阅内容。

另外,我们在浏览一些内容或者博客的时候,也需要一个工具来帮助我们方便的查看和订阅RSS源,这个时候就要推荐一下DIYgod大佬开发的浏览器插件RSSHub-Radar,对于我们的博客,如果已经加了我前面说的html代码,它可以自己发现订阅地址,如下图所示:

它还支持配置规则,则一些拥有RSSHub订阅的站点,比如b站,微博,小红书等,可以嗅探到RSShub的订阅地址,如下图所示:

另外,看上面弹出的窗口中是可以直接去预览对应的RSS内的,还可以直接跳转到Follow、FreshRss等订阅源去添加这个订阅源,这些可以在插件的设置中进行设置,如下图所示:

除了上面的设置,这个插件还支持一些其他的设置,读者朋友可以自行探索。

总结

以上就是关于网站配置和rss订阅方面我的一些建议,而本文的标题也有一些标题党了,欢迎吐槽。

资料

如果读者需要查阅ATOM和RSS的维基百科,请查看英文版本,中文版本内容比较简略,很多发展相关的内容都没有。

看完评论一下吧

Android源码分析:再读消息循环源码

2024-10-10 21:17:16

Android消息循环在应用开发中会经常涉及,我以前也分析过。不过那个时候分析的还是以很老的Android源码来进行的,并且只是分析了Java层的代码,当时的文章为:Android消息循环分析。而Native层,以及一些新增的功能,都没有涉及,今天再读源码,对其进行再次分析。

消息循环简化版本

对于应用层的开发者来说,虽然已经过了10年,java层的Api还是跟之前一样的,依然是通过Handler发送消息,Looper会中消息队列中取消息,消息会根据Handler中的callback或者消息自己的callback执,如上图所示。我之前分析的发送消息和处理消息已经比较清楚了,这块不再看了。这里主要分析一下从MessageQueue取消息,之前涉及的文件描述符的监控和Native层的一些实现等进行分析。

java层loop取消息

首先来看java层如何从消息队列取消息的,Looper中有如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void loop() {  
    final Looper me = myLooper();  
    ...
    me.mInLoop = true;  
	Binder.clearCallingIdentity();  
    final long ident = Binder.clearCallingIdentity();  
    ...
    for (;;) {  
        if (!loopOnce(me, ident, thresholdOverride)) {  
            return;  
        }  
    }  
}

以上代码核心就是拿到当前线程的Looper然后,在无限循环当中取调用loopOnceloopOnce代码很长,但是忽略错误处理和Log,核心代码如下:

1
2
3
4
5
6
7
8
9
private static boolean loopOnce(final Looper me,  
        final long ident, final int thresholdOverride) {  
    Message msg = me.mQueue.next(); //从消息队列中取消息
    ...
    msg.target.dispatchMessage(msg); //分发消息
    ...
    msg.recycleUnchecked(); //回收消息,方便下一次发送消息使用
    return true;
}

loopOnce中主要就是去通过MessageQueue取消息,之后在分发消息,并且回收消息。再来看MessageQueuenext方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Message next() {
	final long ptr = mPtr;
	...
	int nextPollTimeoutMillis = 0;
	for (;;) {
		nativePollOnce(ptr, nextPollTimeoutMillis);
		synchronized (this) {
			Message prevMsg = null;  
			Message msg = mMessages;  
			if (msg != null && msg.target == null) {  
			    do {  
			        prevMsg = msg;  
			        msg = msg.next;  
				} while (msg != null && !msg.isAsynchronous());  
			} 
			if (msg != null) {
				if (now < msg.when) {
					nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
				} else {
					mBlock = false;
					if (preMsg != null) {
						prevMsg.next = msg.next;
					} else {
						mMessages = msg.next;
					}
					msg.next = null;
					msg.markInUse();
					return msg;
				}
			} else {
				nextPollTimeoutMillis = -1;
			}
			...
		}
		...
	}
}

以上为next方法的简化,在Java层的MessageQueue的实现就是一个链表,因此向其中发送消息或者取消息的过程就是链表添加或者删除的过程。在第21行到第26行就是从链表中删除msg的过程。其中这个链表它的头节点是存放在mMessages这个变量,Message在插入链表的时候,也是按照事件先后运行放到链表当中的。

在这个方法的开头,我们看到mPtr,它就是MessageQueue在native层对应的对象,不过Native的Message和Java层的Message是相互独立的,在读取next的时候,也会通过nativePollOnce来native层来读取一个消息,另外在这里还传了一个nextPollTimeoutMillis,用来告诉native需要等待的时间,具体后面在来具体分析相关代码。

因为我们的消息循环中除了放置我们通过Handler所发送的消息之外,还会存在同步信号的屏障,比如ViewRootImpl就会在每一次scheduleTraversals的时候发送一个屏障消息。屏障消息和普通消息的区别就是没有targetHandler。因此在第10行,当我们检查到是屏障消息的时候,会跳过它, 并且查找它之后的第一条异步消息。 另外就是在这个do-while的循环条件中,我们可以看到它还有判断消息是否为Asynchronous的,我们正常创建的Handler一般async都是false,也就是说消息的这个值也是为false。而异步的,一般会被IMS,WMS,Display,动画等系统组件使用,应用开发者无法使用。

这里我们只要知道,如果有异步消息,就会先执行异步消息。在第17行,这里还会判断消息的事件,如果消息的when比当前事件大的化,那么这个消息还不能够执行,这时候需要去等待,这里就会给nextPollTimeoutMillis去赋值。

Native层的MessageQueue和Looper

我们刚刚看MessageQueue的代码时候,看到mPtr,它对应native层的MessageQueue的指针。它的初始化在MessageQueue的构造方法中,也就是调用nativeInit,其内部源码为调用NativeMessageQueue的构造方法,源码在android_os_MessageQueue.cpp中:

1
2
3
4
5
6
7
8
NativeMessageQueue::NativeMessageQueue() :  
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {  
    mLooper = Looper::getForThread();  
    if (mLooper == NULL) {  
        mLooper = new Looper(false);  
        Looper::setForThread(mLooper);  
    }  
}

这里我们可以看到在Native层,创建MessageQueue的时候,也会创建Looper,当然如果当前线程存在Looper则会直接使用。Native层的Looper跟Jav层一样,是存放在ThreadLocal当中的,可以看如下代码:

1
2
3
4
5
sp<Looper> Looper::getForThread() {  
    int result = pthread_once(& gTLSOnce, initTLSKey);  
    Looper* looper = (Looper*)pthread_getspecific(gTLSKey);  
    return sp<Looper>::fromExisting(looper);  
}

到这里,我们知道对于一个启动了消息循环的线程,它在Java层和Native层分别会有各自的MessageQueue和Looper,java层通过mPtr来引用Native层的对象,从而使得两层能够产生联系。

Native层pollOnce

之前分析Java层获取消息的时候,会有一个地方调用nativePollOnce,它在native拿到NativeMessageQueue之后会调用它的pollOnce方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {  
    mPollEnv = env;  
    mPollObj = pollObj;  
    mLooper->pollOnce(timeoutMillis);  
    mPollObj = NULL;  
    mPollEnv = NULL;  
  
    if (mExceptionObj) {  
        env->Throw(mExceptionObj);  
        env->DeleteLocalRef(mExceptionObj);  
        mExceptionObj = NULL;  
    }  
}

这里的pollObj为我们java层的MessageQueue, 这里继续调用了native层的pollOnce,代码如下:

1
2
3
4
5
6
7
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { //我们的调用流程只会传timeoutMillis
	...
	for (;;) {
		...
		result = pollInner(timeoutMillis);
	}
}

这里省略了一些结果处理的代码,我们可以回头在看,这可以看到开启了一个无限循环,并调用pollInner, 这个方法比较长,我们先分块看其中的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {  
    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);  
    int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);  
    if (messageTimeoutMillis >= 0  
            && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {  
        timeoutMillis = messageTimeoutMillis;  
    }
}
int result = POLL_WAKE;  
mResponses.clear();   //清除reponses列表和计数
mResponseIndex = 0;  
mPolling = true;  
  
struct epoll_event eventItems[EPOLL_MAX_EVENTS];  
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);  
  
mPolling = false;

这里timeoutMillis是我们从java层传过来的下一个消息的执行事件,而mNextMessageUptime是native层的最近一个消息的执行事件,这个根据这两个字段判断需要等待的事件。

在之后调用epoll_wait来等待I/O事件,或者到设置的超时时间结束等待,这样做可以避免Java层和Native层的循环空转。此处的epoll_wait除了避免循环空转还有另一个作用,我们之前在分析IMS也使用过LooperaddFd,这里如果对应的文件描述符有变化,这里就会拿到,并反应在eventCount上,这里我们先不具体分析,后面再看。

Native消息的读取和处理

当等待完成之后,就会去native的消息队列中取消息和处理,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Done: ;
	mNextMessageUptime = LLONG_MAX;  
    while (mMessageEnvelopes.size() != 0) {  
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);  
        const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
        if (messageEnvelope.uptime <= now) {  
            { 
                sp<MessageHandler> handler = messageEnvelope.handler;  
                Message message = messageEnvelope.message;  
                mMessageEnvelopes.removeAt(0);  
                mSendingMessage = true;  
                mLock.unlock();    
                handler->handleMessage(message);  
            }  
  
            mLock.lock();  
            mSendingMessage = false;  
            result = POLL_CALLBACK;  
        } else {  
            mNextMessageUptime = messageEnvelope.uptime;  
            break;  
        }  
    }

在Native中消息是放在mMessageEnvelope当中,这是一个verctor也就是一个动态大小的数组。不过不看这个的化,我们可以看到这里读取消息,以及读取它的执行时间uptime跟java层的代码是很像是的,甚至比java层还要简单许多,就是直接拿数组的第一条。之后使用MessageHandler执行handleMessage。这里的MessageHandler跟java层的也是很像,这里再列一下MessageEnvelopeMessage的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct MessageEnvelope {  
    MessageEnvelope() : uptime(0) { }  
  
    MessageEnvelope(nsecs_t u, sp<MessageHandler> h, const Message& m)  
        : uptime(u), handler(std::move(h)), message(m) {}  
  
    nsecs_t uptime;  
    sp<MessageHandler> handler;  
    Message message;  
};

struct Message {  
    Message() : what(0) { }  
    Message(int w) : what(w) { }  
  
    /* The message type. (interpretation is left up to the handler) */  
    int what;  
};

这里和java层的区别是,拆分成了两个结构体,但是呢比java层的还是要简单很多。到这里Native层和Java层对应的消息循环体系就分析完了。但是Native层除了这个消息循环还有一些其他东西,就是前面说到的文件描述符的消息传递。

文件描述符消息读取和处理

前面在pollOnce中还是有关于文件描述符消息的处理,这里继续分析。前面的epoll_wait就会读取相关的事件,读取完事件之后的处理如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (eventCount < 0) {   //如果读出来的eventCount小于0,则说明有错误
    if (errno == EINTR) {  //处理错误,并且跳转到Done去读取native层的消息
        goto Done;  
    }  
    result = POLL_ERROR;  
    goto Done;  
}

if (eventCount == 0) {  //直接超时,没有读到事件
        result = POLL_TIMEOUT;  
        goto Done;  
}

for (int i = 0; i < eventCount; i++) {  //根据返回的条数,来处理消息
    const SequenceNumber seq = eventItems[i].data.u64;  
    uint32_t epollEvents = eventItems[i].events;  
    if (seq == WAKE_EVENT_FD_SEQ) {  //序列为这个序列被定义成为唤醒事件
        if (epollEvents & EPOLLIN) {  
            awoken();  
        } else {  
        }  
    } else {  
        const auto& request_it = mRequests.find(seq);  
        if (request_it != mRequests.end()) {  
            const auto& request = request_it->second;  
            int events = 0;  
            if (epollEvents & EPOLLIN) events |= EVENT_INPUT;  
            if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;  
            if (epollEvents & EPOLLERR) events |= EVENT_ERROR;  
            if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;  
            mResponses.push({.seq = seq, .events = events, .request = request});  
        } else {  
	        ...
        }  
    }  
}

前面的错误处理我们直接看我的注释即可。后面会根据返回的eventCount来一次对每一个eventItem做处理,其他它的u64为序列号,这些为注册到LoopermRequests的序列号,其中1为WAKE_EVENT_FD_SEQ,也就是mWakeEventFd的序列,这里唤醒我们先不管了,直接看后面的正常的文件描述符事件监听。 这里首先会通过seq找到对应的Request,并根据epollEvents来设置他们的事件类型,之后封装成为Response放到mResponses当中。在这些做完,后面同样是跳转到Done后面的代码块,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Done: ;
	...
	for (size_t i = 0; i < mResponses.size(); i++) {  
        Response& response = mResponses.editItemAt(i);  
        if (response.request.ident == POLL_CALLBACK) {  
            int fd = response.request.fd;  
            int events = response.events;  
            void* data = response.request.data;  
            int callbackResult = response.request.callback->handleEvent(fd, events, data);  
            if (callbackResult == 0) {  
                AutoMutex _l(mLock);  
                removeSequenceNumberLocked(response.seq);  
            }  
  
            response.request.callback.clear();  //移除response对与callback的引用
            result = POLL_CALLBACK;  
        }  
    }

这里则是遍历刚刚我们填充的mResponses数组,从其中取出每一个Response,并调用它的Request的Callback回调的handleEvent方法,它的使用我们之前分析IMSServiceManager启动的时候已经见到过了。

以上说的是Java层会初始化Handler和Looper的情况,如果只是Native层使用的话,一般怎么用的呢。我们以BootAnimation中的使用为例,它是在BootAnimation.cpp当中,在初始化BootAnimation对象的时候,会创建一个Looper,代码如下:

1
new Looper(false)

readyToRun中添加文件描述符的监听:

1
2
3
4
5
status_t BootAnimation::readyToRun() {
	...
	mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT, 
        new DisplayEventCallback(this), nullptr);
}

最后去循环调用pollOnce,来获取消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool BootAnimation::android() {
 do {
	 processDisplayEvents();
	 ...
 } while (!exitPending());
}

void BootAnimation::processDisplayEvents() {  
   mLooper->pollOnce(0);  
}

这就是Android Framework当中,大部分的Native场景使用消息循环的方式。而Native中,想要跟Java层一样发送消息,则是调用Looper的sendMessage方法。而Native层的Handler我们可以理解为只是一个Message的回调,和java层的Handler功能不可同日而语。

异步消息

在Java层的消息循环中,消息是有同步和异步之分的,异步消息一般都会伴随则屏障消息,我们之前分析的获取next消息中可以看到,如果第一个消息是屏障消息,会找后面的第一条异步消息来执行。

同时在enqueueMessage的代码中也有如下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//MessageQueue.java
needWake = mBlocked && p.target == null && msg.isAsynchronous();  
Message prev;  
for (;;) {  
    prev = p;  
    p = p.next;  
    if (p == null || when < p.when) {  
        break;  
    }  
    if (needWake && p.isAsynchronous()) {  
        needWake = false;  
    }  
}  
msg.next = p; 
prev.next = msg;

插入异步消息会改变唤醒等待的状态,如果链表头是屏障消息,且之前调用next的时候mBlocked设置为了true,且当前是异步消息会设置成唤醒,但是如果当前的消息队列中已经有了比当前消息更早执行的消息,则不会唤醒。

到这就完成了消息循环的所有分析了。也欢迎读者朋友交流探讨。

看完评论一下吧

安庆游记2-集贤时空和倒扒狮街

2024-10-08 19:19:15

之前的文章分享的是去安庆博物馆,在第二天,朋友推荐了集贤时空这个地方,便约着一起去了。

集贤时空位于集贤北路上,由原来的白鳍豚水泥厂改造而成。进去里面会发现里面的商业设施还不完善,许多空间都还在招租,旁边还有二期正在施工,现在也不需要门票。

停完车,看到入口大门,就能感受到满满的大字报气息。

里面的墙壁也是到处充满了毛主席语录

以及其他一些之前比较流行的标语 最为突出的当属以厂房绘制的大邮筒,2B铅笔等,满满的怀旧风。

除了标语和大字报之外,这里也还有一些别的景观,首先是一个根雕展览,里面充满了各种精美的根雕。

以及古居民去的微缩景观,做的栩栩如生,很漂亮。

这个地方目前免费,人还是挺多的,不知道将来如果收费人流量会怎么样。另外现在这里招商的一些商家的店铺招牌,和这个文创园的风格不太搭配,希望园方能控制一下招商品控吧。外面的停车场很大,管理却不是很好,很多人不会停车,画的线也不看,一个车占用两个车位。

中午去亲戚推荐的当地饭店吃了老母鸡汤泡炒米,饭店味道还不错。下午就去了倒扒狮街,去了才知道,这条街我以前是来过的。不过当时还没有做文化包装,当时里面还主要是一些买服装的店铺。

而现在,这个街经过改造,文艺气息浓厚很多,游客也很多。 墙上海子的诗倒是还是和之前看到的一样。 安庆方言听了几年了,这上面的倒是也能看懂一些。 地名放到一起,也是文艺气息十足。 国庆期间,在倒扒狮街的戏台上和钱牌楼的路边舞台,都有表演,包括相声、杂耍、变脸等,这个要给安庆文旅点个赞。

老城的文化开发搞得挺不错的,吸引一大波人流。但是停车开车太难了,所以城墙,迎江寺都没有去看,下次有机会在看喽。

这边的美食,大家一般都比较喜欢吃油炸的,这里也推荐少年宫路的一家517油炸,亲戚经常带小孩去吃,用的油比较放心,味道也不错,价格也小贵。另外就是少年宫路的好吃的挺多,可以前往探索。

看完评论一下吧

安庆游记1-安庆博物馆

2024-10-07 23:11:36

媳妇家在离安庆不远,过个江就到了,因此安庆经常去,不过去安庆也主要是去逛商场和吃饭。之前呢也去过附近的乌龙溪和菜子湖,安庆市区的除了菱湖公园别的也都没去过了。这次国庆过来,就去了一下安庆博物馆、集贤时空和倒扒狮街。本文先介绍一下安庆博物馆的这一部分。

对于安庆博物馆感兴趣也是今年初朋友提到说这里还不错,可以看看。当时因为时间不赶巧,没能前来。这次有空就专门跑过来转了转。 安庆作为安徽之前的省会,并且安徽的简称皖也是来自安庆境内的皖山和皖河,更早时期这里还是古皖国的范围,安庆还是有很多东西值得看一看的。因为黄梅戏在安庆发扬光大,安庆博物馆也是中国黄梅戏博物馆。 安庆博物馆不收门票,不过需要提前预约,因为不像上海等大城市的博物馆那么热门,门票很好预约。因为博物馆建在新区,这边空间也比较开阔,停车也很方便,并且不要停车费。

我们首先观看的是二楼的钱币陈列馆,在这个馆内可以看到从古至今的各种各样的钱币,了解钱币的发展,同时馆内还陈设了安庆造币厂的模型,以及世界各国的钱币。

随后观看了二楼的安庆古代文明陈列,这里了解了一些安庆的一些遗址,以及安庆在古代的发展,以及各种古代的器皿,工具,艺术品等。 其中印象最深的是了解到原来雷池就在安庆。

同时二楼还有安庆的近代文明,这其中主要就是介绍清朝后期到现在的一些历史,以及一些知名的人物,如李大钊,陈独秀等。而建国之后省会就转到合肥了,建国之后的就少有提及 。这里都是一些名人和历史事件比较多,我也就没拍多少照片了。

三楼首先看的是安庆城市记忆馆,这里可以看到安庆以前作为省会的辉煌。包括当时 安庆城的模型,以及迎江寺的模型。

以前安庆人的日常用品,和绣鞋花包等等。

安庆的邮局和知名品牌店铺的还原(这个现在很多博物馆都很喜欢搞,之前在南京博物院也见到过类似的)。

三楼的另一重磅则是黄梅戏艺术陈列馆。入口设计的就很漂亮。

这个馆内主要展示来黄梅戏的又来,以及安庆当地的一些戏曲品种,以及 他们如何最终演化成为黄梅戏。同时还要黄梅戏的服装,道具,乐器等等的展示。

甚至呢,很多的木雕也有戏曲表演的场景。

在一楼首先就是最近的临展,鲁迅的艺术世界,这个展是和北京鲁迅博物馆合办,很多文物也是来自鲁迅博物馆。这里给我印象最深的就是展示了很多鲁迅收藏的艺术作品,版画,剪纸之类的,还有鲁迅所设计的图书封面等。同时这个展馆的海报,内部布置风格也很鲁迅,看了很喜欢。缺点就是展品数量不是特别多。

另外靠近出口的地方还有一个临展,为安庆和美乡村书画摄影展,通过这些摄影和书画作品可以了解到安庆下面的乡村的美丽,因为时间有限只看了一小部分。

最后就是去在博物馆的商店购买纪念品和盖章,结束安庆博物馆之旅。因为时间有限,这里还有三楼的安庆政协发展陈列(门口看到很政治),一楼的安庆的书画陈列馆,感兴趣的也可以看看。

看完评论一下吧

九月月报-向上冲向前进

2024-10-01 19:10:11

九月学生开学,台风来袭,合肥地震,股票上涨。开了一夜的车到达皖南农村后,稍作休息,有空来写一写九月的月报。

游玩

九月没有出远门去玩,月初周末去了一次崇明岛,东滩湿地公园转了一下,这里是鸟类保护生态园,但是确没看到什么鸟,里面的一些设施也有一些荒废,不过环境还不错,到这露营还不错。另外回程在江边看日落,夕阳下的长江大桥很美。

扬子鳄繁衍地但是没看到鳄 黑天鹅与鹅 落日下的飞机 后面,就跟朋友在松江随便找个路边绿地露营,感觉也不错。这点来说松江的绿化和环境是真不错。 路边绿地风景 中秋节,赶回老家,得以躲过台风“贝碧嘉”。在老家就是在田里转一转。抽了半天去了一趟邻县的沱湖,虽说离得不算远,但是长大几十岁之前也未曾来过。来的时间不是很好,荷花都没了,湖面也不是很好看。这里也盛产大闸蟹,没有阳澄湖出名,不过也很不错。湖边街道两边许多店铺出售蟹,因为蟹还每到丰收季节,于是买了一些母蟹晚上大吃一顿。 沱湖 沱湖残荷 后半个月因为又有台风普拉桑,后面就一直未出门喽。

这个月仍然继续在阅读Android源码,Activity启动重新完整分析了,wms,IMS等代码也有所分析,相关内容也整理成了博文,共有10篇相关博文,感兴趣的可以访问查看Android源码解析

另外最近突然又想学学英语,就下载了多邻国,目前坚持一周了。应用内容还算比较简单,不过游戏化的内容,加上一些激励设计,还挺有意思的。英语学了很多年了,每次都半途而废,立个flag,希望能坚持下去。

投资

美联储降息,国内降存款准备金,降房贷利率等等新政策。全球证券市场和虚拟币基本都上涨了一波。其中虚拟币短期呢波动巨大,一直想要等到回调上车,也没等到机会,只好等下一次机会了。而山寨币,则是币圈暴富的秘籍,但是波动巨大不是一般人能玩的。

而A股,之前买股票一直亏损后,就转投指数基金了,之前的亏损在经历这次上涨总算是浮盈了,一些条件单也出掉落袋为安了。各个群里的群友这几天还在杀入,我却也是未加仓,虽然说大水漫灌,但是经济环境似乎并不会很快变化,如果说是牛市来了,那也与我无关。

本月阅读不多,上个月未读完的《西藏西藏》给读完了。又新开始读了一本《简读中国史》,以全球视角来看中国史,另外把中国的不同朝代放到一起分析,分析封建制度的演进变化,目前还没读完,不敢评价说书多好,但是这样的分析视角却是便于我们去更好的认识历史。

电视剧看了一个《月光花谋杀案》,是我喜欢的侦探案件,六集的长度,也不至于花很多时间。剧中小说以剧中案件为基础创作,以小说来引导剧中案件的调查,形式新颖,推荐一看。 电影看了两个。《默杀》和《第二十条》,这两部剧的共同点都是有校园暴力,不过前者是以这个为主线,讨论人性。而后者,则是有一定的喜剧成分,但是讨论的是正当防卫和故意伤害,最后升华了主题。

尾声

台风是强烈的,股票是上涨的,生活是平淡的,九月就这样过去了。我们喜迎国庆,也祝福十月股票继续大涨。

这是我的第四个月月报,下个月再见。最后祝大家国庆节快乐。

看完评论一下吧

Android源码分析:系统进程中事件的读取与分发

2024-09-26 13:44:04

之前分析的是从InputChannel中读取Event,并且向后传递,进行消费和处理的过程。但是之前的部分呢,事件如何产生,事件怎么进入到InputChanel当中的,事件又是如何跨进程到达App进程,这里继续来分析。

以上为system进程的流程的简化图,这里我们可以看到几个重要的组件,这里以触摸事件来进行分析(后文的分析也将会以触摸事件为主进行分析)并且简单的描绘了事件从EventHub到服务端的InputChannel发送事件的全部过程。具体内容一起来看下面的代码。

InputManagerService的创建

因为事件的分发涉及到不少类,我们先从InputManagerService(IMS)的初始化出发,进行分析。入口代码在SystemServer.java中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
WindowManagerService wm = null;  
InputManagerService inputManager = null;
inputManager = new InputManagerService(context);
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,  
        new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);  
ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,  
        DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);  
ServiceManager.addService(Context.INPUT_SERVICE, inputManager,  
        /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);

inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());  
inputManager.start();
}

这里我们可以看到WMS的创建我们传入了IMS,并且IMS也依赖WindowMnagerCallbacks,我们先看一下IMS的构造方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public InputManagerService(Context context) {  
    this(new Injector(context, DisplayThread.get().getLooper()));  
}  
  
@VisibleForTesting  
InputManagerService(Injector injector) {  
    ... 
    mHandler = new InputManagerHandler(injector.getLooper());  
    mNative = injector.getNativeService(this);  
    ...
}

我们主要关注这个mNative的构建,它是NativeImpl,它的创建过程如下:

1
new NativeInputManagerService.NativeImpl(service, mContext, mLooper.getQueue());

这里的Looper是前面传进来的DisplayThread的Looper。在NativeImpl的构造方法中调用了init方法,并获取到了它的native指针,这里需要看com_android_server_input_InputManagerService.cpp中的natvieInit方法,代码如下:

1
2
3
4
5
6
7
8
static jlong nativeInit(JNIEnv* env, jclass /* clazz */,  
        jobject serviceObj, jobject contextObj, jobject messageQueueObj) {  
    sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);  
    NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,  
            messageQueue->getLooper());  
    im->incStrong(0);  
    return reinterpret_cast<jlong>(im);  
}

这里创建了NativeInputManager

NativeInputManager初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
NativeInputManager::NativeInputManager(jobject contextObj,  
        jobject serviceObj, const sp<Looper>& looper) :  
        mLooper(looper), mInteractive(true) {  
    JNIEnv* env = jniEnv();  
  
    mServiceObj = env->NewGlobalRef(serviceObj);  
  
    {  
        AutoMutex _l(mLock);  
        mLocked.systemUiLightsOut = false;  
        mLocked.pointerSpeed = 0;  
        mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION;  
        mLocked.pointerGesturesEnabled = true;  
        mLocked.showTouches = false;  
        mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT;  
    }  
    mInteractive = true;  
  
    InputManager* im = new InputManager(this, this);  
    mInputManager = im;  
    defaultServiceManager()->addService(String16("inputflinger"), im);  
}

这个构造方法中,传入的jobject为我们之前的NativeImpl,后面有需要调用java层的时候会用到它。除此之外我们看到又创建了一个InputManger,并且把它注册到了ServiceManger当中,名称为inputflinger

我们继续看InputManager的初始化代码,它传如的两个参数readerPolicydispatcherPolicy的实现都在NativeInputManager当中。它的代码如下:

1
2
3
4
5
6
7
8
InputManager::InputManager(  
        const sp<InputReaderPolicyInterface>& readerPolicy,  
        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {  
    mDispatcher = createInputDispatcher(dispatcherPolicy);  
    mClassifier = std::make_unique<InputClassifier>(*mDispatcher);  
    mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mClassifier);  
    mReader = createInputReader(readerPolicy, *mBlocker);  
}

这里首先创建了InputDispatcher,之后创建的mClassifiermBlockerInputDispatcher一样都是继承自InputListenerInterface,它们的作用为在事件经过InputDispatcher分发之前,可以做一些预处理。最后创建InputReader,事件会经由它传递到InputDispatcher,最后再由InputDispatcher分到到InputChannel。下面来详细分析。

事件源的初始化

因为InputDispatcher初始化代码比较简单,我们从createInputReader的源码开始看起来:

1
2
3
4
std::unique_ptr<InputReaderInterface> createInputReader(  
        const sp<InputReaderPolicyInterface>& policy, InputListenerInterface& listener) {  
    return std::make_unique<InputReader>(std::make_unique<EventHub>(), policy, listener);  
}

我们可以看到在创建InputReader之前首先创建了一个EventHub,看名字我们就知道它是一个事件的收集中心。我们看它的构造方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
EventHub::EventHub(void)  
      : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),  
        mNextDeviceId(1),  
        ...
        mPendingINotify(false) {  
    ensureProcessCanBlockSuspend();  
  
    mEpollFd = epoll_create1(EPOLL_CLOEXEC);  //创建epoll实例,flag表示执行新的exec时候会自动关闭
  
    mINotifyFd = inotify_init1(IN_CLOEXEC);  //创建inotify实例,该实例用于监听文件的变化
  
    if (std::filesystem::exists(DEVICE_INPUT_PATH, errorCode)) {  
        addDeviceInputInotify();  
    } else {  
        addDeviceInotify();  
        isDeviceInotifyAdded = true;  
    
    }  
  
    struct epoll_event eventItem = {};  
    eventItem.events = EPOLLIN | EPOLLWAKEUP;  
    eventItem.data.fd = mINotifyFd;  
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);  
    int wakeFds[2];  
    result = pipe2(wakeFds, O_CLOEXEC);  
    
    mWakeReadPipeFd = wakeFds[0];  
    mWakeWritePipeFd = wakeFds[1];  
  
    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);  
    
    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);  
    
    eventItem.data.fd = mWakeReadPipeFd;  
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);  
}

从上面的代码我们可以看到这里主要为创建inotify并且通过epoll去监听文件的变化,其中还是用管道创建了wakeReadPipewakeReadPipe的文件描述,用于接收回调。我们先看一下addDeviceInputInotify()方法:

1
2
3
4
void EventHub::addDeviceInputInotify() {  
    mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);  
    
}

其中DEVICE_INPUT_PATH的值为/dev/input,也就是说把这个path放到mINofiyFd的监控当中。对于了解Linux的人应该知道,在Linux中万物结尾文件,因此我们的输入也是文件,当事件发生的时候便会写入到/dev/input下面,文件变化我们也会得到通知。我这里使用ls命令打印了一下我的手机,/dev/input下面有如下文件:

1
event0  event1  event2  event3  event4  event5

具体这些文件的写入,那就是内核和驱动相关的东西了,我们这里不再讨论。而事件的读取,我们后面再进行分析。

IMS的启动

各个对象都构建完成之后,IMS要进行启动,才能够对事件进行处理并且分发。SystemServer中已经调用了IMS的start方法,它其中又会调用NativeInputMangerstart方法,最终会调用 native层的InputManagerstart方法。而其中分别又调用了Dispatcher的start方法和Reader的start方法。我们分别分析。

InputDispater 调用start

1
2
3
4
5
6
7
8
status_t InputDispatcher::start() {  
    if (mThread) {  
        return ALREADY_EXISTS;  
    }  
    mThread = std::make_unique<InputThread>(  
            "InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });  
    return OK;  
}

这个方法中主要创建了InputThread,并且给它传了两个lambda,分别执行InputDispatchdispatchOnce方法和执行Looper的wake方法。我们看InputThread的构造方法:

1
2
3
4
5
InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake)  
      : mName(name), mThreadWake(wake) {  
    mThread = new InputThreadImpl(loop);  
    mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);  
}

可以看到其中创建了InputThreadImpl,这个类才是真的继承的系统的Thread类,这里构建完成它就继续调用了它的run方法,这样它就会启动了。这里我们需要注意这个 线程的优先级,为PRIORITY_URGEN_DISPLAY,可以看到优先级是非常高了。

1
2
3
4
bool threadLoop() override {  
    mThreadLoop();  
    return true;  
}

另外就是我们传进来的loop传入了这个对象,并且在它的threadLoop中会执行它。对于native中的线程,我们在threadLoop中实现逻辑就可以了,并且这里我们返回值为true,它会继续循环执行 。而我们传入的另一个lambda,则是在线程推出的时候调用。这个线程循环中执行的就是我们的InputDispatch中 的dispatchOnce方法,也就是消息的投递,后面再来分析。

InputReader调用start方法

1
2
3
4
5
6
7
8
status_t InputReader::start() {  
    if (mThread) {  
        return ALREADY_EXISTS;  
    }  
    mThread = std::make_unique<InputThread>(  
            "InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });  
    return OK;  
}

这里的初始化,我们可以看到跟前面的InputDispatch很类似,连InputThread用的都是同一个类,内部也就一样有InputThreadImpl了。这里则是调用了InputReader内部的loopOnce方法。到这里系统就完成了输入事件分发的初始化了。

我们在看事件的分发之前,先看一下应用中的接收和系统的InputDispatch进行连接的过程。

InputChannel的注册

我们之前分析应用层的事件传递的时候,只是谈到了InputChannel是在WMS调用如下代码生成的:

1
mInputChannel = mWmService.mInputManager.createInputChannel(name); 

但是内部如何创建InputChannel的,以及 这个InputChannel是如何收到消息的我们都没有涉及,我们现在继续分析它一下。这个createInputChannel内部最终会调用到native层的InputDispatchercreateInputChannel方法, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(const std::string& name) {  
    std::unique_ptr<InputChannel> serverChannel;  
    std::unique_ptr<InputChannel> clientChannel;  
    status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);  
    ...
    { // acquire lock  
        std::scoped_lock _l(mLock);  
        const sp<IBinder>& token = serverChannel->getConnectionToken();  
        int fd = serverChannel->getFd();  
        sp<Connection> connection =  
                n1ew Connection(std::move(serverChannel), false /*monitor*/, mIdGenerator);  
        ...
        mConnectionsByToken.emplace(token, connection);  
  
        std::function<int(int events)> callback = std::bind(&InputDispatcher::handleReceiveCallback, this, std::placeholders::_1, token);  
  
        mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, new LooperEventCallback(callback), nullptr);  
    } // release lock  
  
    // Wake the looper because some connections have changed.    
    mLooper->wake();  
    return clientChannel;  
}

首先是第4行代码,这里创建了InputChannel,而它又分为serverChannelclientChannel,返回调用方的是`clientChannel。

我们先进去看看其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
status_t InputChannel::openInputChannelPair(const std::string& name,  
                                            std::unique_ptr<InputChannel>& outServerChannel,  
                                            std::unique_ptr<InputChannel>& outClientChannel) {  
    int sockets[2];  
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {  //创建socket对
	    ..
        return result;  
    }  
    ..
    sp<IBinder> token = new BBinder();  
  
    std::string serverChannelName = name + " (server)";  
    android::base::unique_fd serverFd(sockets[0]);  //获取server socket fd
    outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token);  //创建server InputChannel
  
    std::string clientChannelName = name + " (client)";  
    android::base::unique_fd clientFd(sockets[1]);  
    outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token);  //创建Client InputChannel
    return OK;  
}

以上代码我们可以看到就是创建了一对socket,分别放到两个InputChannel当中,并且这里创建了一个BBinder作为两个InputChannel的token,具体用处我们后面会再提到。此时可以继续回看前面的createInputChannel方法,在11行,创建了一个 Connection对象,并且以前面创建的BBinder为key放到了mConnectionsByToken当中,Connection的用处留到后面继续讲。

在15行创建了一个callback,其中会执行InputDispatcherhandleReceiveCallback方法,并且这个callback被添加looper的addFd的时候设置进去了,这里的fd就是之前创建的ServerInputChannel的socket的文件描述符。到这里就完成了初始化,添加了服务端InputChannel的文件描述符监听。

事件触发

我们之前在分析InputManger的启动的时候,已经看到了事件是通过/dev/input来通知到EventHub,而InputReader通过Looper监听了/dev/input的文件描述符,从而让我们事件传递的系统动起来。那么我们首先就从InputReaderloopOnce开始看起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void InputReader::loopOnce() {  
	...
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);  
  
    { // acquire lock  
        std::scoped_lock _l(mLock);  
        mReaderIsAliveCondition.notify_all();  
  
        if (count) {  
            processEventsLocked(mEventBuffer, count);  
        }  
        ...
    } // release lock  
    ...
    mQueuedListener.flush();  
}

我们这里省略了设备变化,超时等相关的代码,仅仅保留了事件读取相关的部分。我们看到,首先在第3行中,从EventHub中去获取新的事件,之后在第10行,去处理这些事件,第15行会清楚所有的事件,我们分别看看各个里面的逻辑。

从EventHub读取事件

首先是getEvents方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {  
    std::scoped_lock _l(mLock);  
    struct input_event readBuffer[bufferSize];  
    RawEvent* event = buffer;  
    size_t capacity = bufferSize;  
    bool awoken = false;  
    for (;;) {  
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);  

        bool deviceChanged = false;  
        //pendingIndex小于PendingCount,说明之前有事件还为处理完
        while (mPendingEventIndex < mPendingEventCount) {  
            const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];  
            ...
            Device* device = getDeviceByFdLocked(eventItem.data.fd);  
            if (device == nullptr) {  //未能找到device,报错跳出
                continue;  
            }  
            
            // EPOLLIN表示有事件可以处理
            if (eventItem.events & EPOLLIN) {  
                int32_t readSize =  
                        read(device->fd, readBuffer, sizeof(struct input_event) * capacity);  
                if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {  
                    // 接收到通知之前,设备以及不见了
                    deviceChanged = true;  
                    closeDeviceLocked(*device);  
                } else if() { //其中的错误情况,忽略掉
                } else {  
                    int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;  
  
                    size_t count = size_t(readSize) / sizeof(struct input_event);  //根据一个事件的大小,来算同一个设备上面读取到的事件的个数
                    //以下为具体保存事件到event当中
                    for (size_t i = 0; i < count; i++) {  
                        struct input_event& iev = readBuffer[i];  
                        event->when = processEventTimestamp(iev);  
                        event->readTime = systemTime(SYSTEM_TIME_MONOTONIC);  
                        event->deviceId = deviceId;  
                        event->type = iev.type;  
                        event->code = iev.code;  
                        event->value = iev.value;  
                        event += 1;  
                        capacity -= 1;  
                    }  
                    if (capacity == 0) {  //缓冲区已经满了,无法在记录事件,跳出
                        mPendingEventIndex -= 1;  
                        break;  
                    }  
                }  
            } else if (eventItem.events & EPOLLHUP) {  
               ... 
            } else {  
	            ...
            }  
        }  
        ...
        //event和buffer地址不同说明已经拿到事件了,可以跳出循环 
        if (event != buffer || awoken) {  
            break;  
        }  

        mPendingEventIndex = 0;  
        mLock.unlock(); // poll之前先加锁
        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);  
        mLock.lock(); // poll完之后从新加锁
  
        if (pollResult == 0) {  
            // Timed out.  
            mPendingEventCount = 0;  
            break;  
        }  
  
        if (pollResult < 0) {  
            mPendingEventCount = 0;  
            if (errno != EINTR) {  
                usleep(100000);  
            }  
        } else {  
            mPendingEventCount = size_t(pollResult);  
        }  
    }  
  
    // event为填充之后的指针地址,而buffer为开始的地址,相减获得count
    return event - buffer;  
}

这个方法是很复杂的,但是我们主要分析事件的分发,因此其中关于设备变化,设备响应,错误处理等等相关的代码都省略了。这个方法,我们传入了一个RawEvent的指针用来接收事件,另外传了bufferSize来表示我们所能接收的事件数量。这个方法使用了两层循环来进行逻辑的处理,外层的为无限循环。当我们第一次进入这个方法当中,mPendingEventCountmPendingEventIndex都是0,因此不会进入第二层的循环,这个时候会执行到64行,调用epoll_wait系统调用,去读取事件,读取的结果会放到mPendingEventItems当中,之后会算出pendingCount。这样继续循环,我们就可以进入内存循环当中了。 在刚刚的PendingEventItem中并没有存储具体的事件,而是存储的事件发生的设备文件描述符,在内存的循环中,首先会根据设备的描述符查找设备,并对其进行检查。之后再从设备当中读取事件,拼装成为需要向后分发的事件。 这里的count有点让人迷糊,我画了个图如下所示:

其中我们真正读取的事件的数量,是要看有几个设备,每个设备有多少个事件,对其进行计算。 到这里我们就获取到了事件,这里可以回到InputReader中继续往下看了。

InputReader对事件进行处理

在这里的处理调用的是processEventsLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {  
    for (const RawEvent* rawEvent = rawEvents; count;) {  
        int32_t type = rawEvent->type;  
        size_t batchSize = 1;  
        //如果不是设备处理相关的事件,则执行。
        if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {  
            int32_t deviceId = rawEvent->deviceId;  
            while (batchSize < count) {   
                if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT ||  
                    rawEvent[batchSize].deviceId != deviceId) {  
                    //当遇到设备整删除事件,或者不是当前设备的事件,就不能进行批量处理,跳过。
                    break;  
                }  
                batchSize += 1;  
            }  
            processEventsForDeviceLocked(deviceId, rawEvent, batchSize);  
        } else {  
            //设备添加删除之类的事件处理,跳过
        }  
        count -= batchSize;  
        rawEvent += batchSize;  
    }  
}

这个方法中主要是对与设备增加删除事件和普通事件进行分别处理,如果是普通的事件,会对同一个设备上的事件进行批量处理,批量处理则会调用processEventsForDeviceLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void InputReader::processEventsForDeviceLocked(int32_t eventHubId, const RawEvent* rawEvents,  
                                               size_t count) {  
    auto deviceIt = mDevices.find(eventHubId);  
    if (deviceIt == mDevices.end()) {  
        //没有找到设备,返回
        return;  
    }  
  
    std::shared_ptr<InputDevice>& device = deviceIt->second;  
    if (device->isIgnored()) {  //是被忽略的设备,跳过
        return;  
    }  
  
    device->process(rawEvents, count);  
}

这个方法中主要是查找设备,找到未忽略的设备则会调用设备的process方法进行处理。 InputDevice只是设备的抽象,而其中的处理又会调用InputMapper的方法,InputMapper是抽象类,它有许多的实现,比如我们的触摸事件就会有TouchuInputMapperMultiTouchInputMapper,各种不同的InputMapper会对事件进行处理,拼装成符合相关类型的事件,其中逻辑我们就不继续进行追踪了。

对于touch事件,这个process处理完成,在TouchInputMapper中最终会调用dispatchMotion,这个方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void TouchInputMapper::dispatchMotion(...) {  
    PointerCoords pointerCoords[MAX_POINTERS];  
    PointerProperties pointerProperties[MAX_POINTERS];  
    uint32_t pointerCount = 0;  
    ...
    const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);  
    const int32_t deviceId = getDeviceId();  
    std::vector<TouchVideoFrame> frames = getDeviceContext().getVideoFrames();  
    std::for_each(frames.begin(), frames.end(),  
                  [this](TouchVideoFrame& frame) { frame.rotate(this->mInputDeviceOrientation); });  
    NotifyMotionArgs args(getContext()->getNextId(), when, readTime, deviceId, source, displayId,  
                          policyFlags, action, actionButton, flags, metaState, buttonState,  
                          MotionClassification::NONE, edgeFlags, pointerCount, pointerProperties,  
                          pointerCoords, xPrecision, yPrecision, xCursorPosition, yCursorPosition,  
                          downTime, std::move(frames));  
    getListener().notifyMotion(&args);  
}

其中有许多关于多点触控,事件处理的判断,这里只关注最后的部分,就是将事件组装成一个NotifyMotionArgs对象,并调用ListenernotifyMotion方法。这里的getListener()内部首先会调用getContenxt获取Context,而这个Context就是InputReader的内部成员mContext,这这个Listener也就是我们之前在初始化InputReader时候它的成员变量mQueuedListener,那我们下面继续去看它的notifyMotion

notifyMotion

1
2
3
4
void QueuedInputListener::notifyMotion(const NotifyMotionArgs* args) {  
    traceEvent(__func__, args->id);  
    mArgsQueue.emplace_back(std::make_unique<NotifyMotionArgs>(*args));  
}

这里是直接把之前的那个变量放到mArgsQueue当中了。这个时候,我们需要留意一下之前InputReadeloopOnce的15行,这里调用的 flush方法,也是这个QueuedInputListener内部的:

1
2
3
4
5
6
void QueuedInputListener::flush() {  
    for (const std::unique_ptr<NotifyArgs>& args : mArgsQueue) {  
        args->notify(mInnerListener);  
    }  
    mArgsQueue.clear();  
}

这里这是掉用了我们传进来的NotifyArgs的notify方法,并且传过来的参数mInnerListener是我们之前创建InputManager时候创建的,这里会有三层嵌套,首先是UnWantedInteractionBlocker先处理,之后它会按情况传递给InputClassifier处理,最后是在InputDispatcher当中处理。

我们先看看看notify当中做了什么,再继续往后看。

1
2
3
void NotifyMotionArgs::notify(InputListenerInterface& listener) const {  
    listener.notifyMotion(this);  
}

这里也是比较简单,就是直接调用了linster的notifyMotion方法,我们可以直接去看了。因为我们主要关注 传递,而不关注处理,这里就跳过,直接看InputDispatcher中的这个方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {    
    if (!validateMotionEvent(args->action, args->actionButton, args->pointerCount,  
                             args->pointerProperties)) {  
        return;  //不合法的触摸事件直接返回 
    }  
  
    uint32_t policyFlags = args->policyFlags;  
    policyFlags |= POLICY_FLAG_TRUSTED;  
  
    android::base::Timer t;  
   
    bool needWake = false;  
    { // acquire lock  
        mLock.lock();  
        ...
        std::unique_ptr<MotionEntry> newEntry =  
                std::make_unique<MotionEntry>(args->id, args->eventTime, args->deviceId,  
                                              args->source, args->displayId, policyFlags,  
                                              args->action, args->actionButton, args->flags,  
                                              args->metaState, args->buttonState,  
                                              args->classification, args->edgeFlags,  
                                              args->xPrecision, args->yPrecision,  
                                              args->xCursorPosition, args->yCursorPosition,  
                                              args->downTime, args->pointerCount,  
                                              args->pointerProperties, args->pointerCoords);  
        ...
        needWake = enqueueInboundEventLocked(std::move(newEntry));  
        mLock.unlock();  
    } // release lock  
  
    if (needWake) {  
        mLooper->wake();  
    }  
}

在这里则是执行完一些检查之后,把事件封装成为MotionEntry,调用enqueueInboundEventLocked,最后调用looperwake方法。enqueueInboundEventLocked代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
bool InputDispatcher::enqueueInboundEventLocked(std::unique_ptr<EventEntry> newEntry) {  
    bool needWake = mInboundQueue.empty();  
    mInboundQueue.push_back(std::move(newEntry));  
    EventEntry& entry = *(mInboundQueue.back());  
    switch (entry.type) {  
        case EventEntry::Type::MOTION: {  
            if (shouldPruneInboundQueueLocked(static_cast<MotionEntry&>(entry))) {   //返回true的时候,事件会被移除不处理
                mNextUnblockedEvent = mInboundQueue.back();  
                needWake = true;  
            }  
            break;  
        }  
        ...
    }  
  
    return needWake;  
}

在这里,首先把事件放入mInboundQueue这个deque当中,最后根据事件的类型和信息要不要唤醒looper,如果事件不被移除needWake就为false,前面的wake也不会被调用。但是这个是否调用,不影响我们的后续分析,因为InputDispatch中的Thead会一直循环调用。

InputDispatcher分发消息

说到这里,我们就该来看看InputDispatcherdispatchOnce方法了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void InputDispatcher::dispatchOnce() {  
    nsecs_t nextWakeupTime = LONG_LONG_MAX;  
    { // acquire lock  
        std::scoped_lock _l(mLock);  
        mDispatcherIsAlive.notify_all();  
  
        if (!haveCommandsLocked()) {  
            dispatchOnceInnerLocked(&nextWakeupTime);  
        }  
  
        if (runCommandsLockedInterruptable()) {  
            nextWakeupTime = LONG_LONG_MIN;  
        }  
        ...
    } // release lock  
  
    //等待下一次调用
    mLooper->pollOnce(timeoutMillis);  
}

这里有不少处理下一次唤醒的逻辑,我们都跳过,主要就看一下第8行,进行这一次的实际执行内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {  
	...
    if (!mPendingEvent) {  //当前没有待处理的pending事件
        if (mInboundQueue.empty()) {  //如果事件列表为空
            ... 
            if (!mPendingEvent) {  
                return;  
            }  
        } else {  
            // 列表中拿一个事件
            mPendingEvent = mInboundQueue.front();  
            mInboundQueue.pop_front();  
            traceInboundQueueLengthLocked();  
        }  
        ...
    }  
  
    bool done = false;  
    ..
    switch (mPendingEvent->type) {  
		...
        case EventEntry::Type::MOTION: {  
            std::shared_ptr<MotionEntry> motionEntry =  
                    std::static_pointer_cast<MotionEntry>(mPendingEvent);  
            ...
            done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);  
            break;  
        }  
        ...
    }  
  
    if (done) {  
        ...
        releasePendingEventLocked();  
        *nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately  
    }  
}

我们将这个方法进行了简化,仅仅保留了触摸事件的部分代码。首先判断mPendingEvent是否为空,为空的时候我们需要到mPendingEvent中去拿一个,我们之前插入的是尾部,这里是从头部取的。拿到事件进行完种种处理和判断之后,会调用dispatchMotionLocked进行触摸事件的分发:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,  
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {  
    if (!entry->dispatchInProgress) {   //设置事件正在处理中
        entry->dispatchInProgress = true;  

    }  
  
    if (*dropReason != DropReason::NOT_DROPPED) {  
        //对于要抛弃的事件这里进行处理,返回
        return true;  
    }  
  
    const bool isPointerEvent = isFromSource(entry->source, AINPUT_SOURCE_CLASS_POINTER);  //读取是否为POINTER
    std::vector<InputTarget> inputTargets;  
  
    bool conflictingPointerActions = false;  
    InputEventInjectionResult injectionResult;  
    if (isPointerEvent) {  
	    //如果屏幕触摸事件则去找到对应的window
        injectionResult =  
                findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,  
                                               &conflictingPointerActions);  
    } else {  
        // Non touch event.  (eg. trackball)  
        injectionResult =  
                findFocusedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime);  
    }  
    if (injectionResult == InputEventInjectionResult::PENDING) {  
        return false;  
    }  
  
    setInjectionResult(*entry, injectionResult);  
    ...
  
    // Dispatch the motion.  
    dispatchEventLocked(currentTime, entry, inputTargets);  
    return true;  
}

这个方法中依然是对于事件做很多的处理和判断,比如否要抛弃等。但是其中最终要的是调用findFocusedWIndowTargetsLocked来找到我们的事件所对应的Window,并且保存相关信息到inputTargets当中,这里获取inputTargets的过程比较复杂,但是简单来说呢就是从之前我们保存在InputDispatcher中的mConnectionsByToken中查找到对应的条目,这里暂不深入分析。拿到这个之后就是调用dispatchEventLocked去分发,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,  
                                          std::shared_ptr<EventEntry> eventEntry,  
                                          const std::vector<InputTarget>& inputTargets) {  
    ...
    for (const InputTarget& inputTarget : inputTargets) {  
        sp<Connection> connection =  
                getConnectionLocked(inputTarget.inputChannel->getConnectionToken());  
        if (connection != nullptr) {  
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);  
        } else {  
            
        }  
    }  
}

通过这里我们可以看到首先是通过inputTarget去拿到connectionToken,再通过它拿到Connection。最后通过调用prepareDispatchCycleLocked

1
2
3
4
5
6
7
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,  
                                                 const sp<Connection>& connection,  
                                                 std::shared_ptr<EventEntry> eventEntry,  
                                                 const InputTarget& inputTarget) {  
    ...
    enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);  
}

这个方法简化的化,这是调用第6行的这个方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,  
                                                   const sp<Connection>& connection,  
                                                   std::shared_ptr<EventEntry> eventEntry,  
                                                   const InputTarget& inputTarget) {  
  
    bool wasEmpty = connection->outboundQueue.empty();  
  
    // Enqueue dispatch entries for the requested modes.  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_OUTSIDE);  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_IS);  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);  
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,  
                               InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);  
  
    // If the outbound queue was previously empty, start the dispatch cycle going.  
    if (wasEmpty && !connection->outboundQueue.empty()) {  
        startDispatchCycleLocked(currentTime, connection);  
    }  
}

其中对于消息会尝试按照每一种mode都调用enqueueDIspatchEntryLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection,  
                                                 std::shared_ptr<EventEntry> eventEntry,  
                                                 const InputTarget& inputTarget,  
                                                 int32_t dispatchMode) {  
    
    int32_t inputTargetFlags = inputTarget.flags;  
    if (!(inputTargetFlags & dispatchMode)) {  
        return;  
    }  
    inputTargetFlags = (inputTargetFlags & ~InputTarget::FLAG_DISPATCH_MASK) | dispatchMode;  
  
    std::unique_ptr<DispatchEntry> dispatchEntry =  
            createDispatchEntry(inputTarget, eventEntry, inputTargetFlags);  
  
    EventEntry& newEntry = *(dispatchEntry->eventEntry);  
    // Apply target flags and update the connection's input state.  
    switch (newEntry.type) {  
        case EventEntry::Type::MOTION: {  
            const MotionEntry& motionEntry = static_cast<const MotionEntry&>(newEntry);  
            constexpr int32_t DEFAULT_RESOLVED_EVENT_ID =  
                    static_cast<int32_t>(IdGenerator::Source::OTHER);  
            dispatchEntry->resolvedEventId = DEFAULT_RESOLVED_EVENT_ID;  
            ...
            
            
            if ((motionEntry.flags & AMOTION_EVENT_FLAG_NO_FOCUS_CHANGE) &&  
                (motionEntry.policyFlags & POLICY_FLAG_TRUSTED)) {  
                break;  
            }  
  
            dispatchPointerDownOutsideFocus(motionEntry.source, dispatchEntry->resolvedAction,  
                                            inputTarget.inputChannel->getConnectionToken());  
            break;  
        }  
        ...
    }  
    connection->outboundQueue.push_back(dispatchEntry.release());  
    traceOutboundQueueLength(*connection);  
}

在这个方法中,又把事件封装成为dispatchEntry,并放到Connection内部的outboundQueue这个队列当中。

到这里我们可以回看上面的enqueueDispatchEntriesLocked的最后一块代码,那里有判断了如果这个outboundQueue队列不为空,则会执行最后的startDispatchCycleLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,  
                                               const sp<Connection>& connection) {  
    
    while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {  
        DispatchEntry* dispatchEntry = connection->outboundQueue.front();  
        dispatchEntry->deliveryTime = currentTime;  
        ...
        // Publish the event.  
        status_t status;  
        const EventEntry& eventEntry = *(dispatchEntry->eventEntry);  
        switch (eventEntry.type) {   
            ...
            case EventEntry::Type::MOTION: {  
                const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);  
                ... 
  
                std::array<uint8_t, 32> hmac = getSignature(motionEntry, *dispatchEntry);  
  
                status = connection->inputPublisher  
                                 .publishMotionEvent(dispatchEntry->seq,  
                                                     dispatchEntry->resolvedEventId,  
                                                     motionEntry.deviceId, motionEntry.source,  
                                                     motionEntry.displayId, std::move(hmac),  
                                                     dispatchEntry->resolvedAction,  
                                                     motionEntry.actionButton,  
                                                     dispatchEntry->resolvedFlags,  
                                                     motionEntry.edgeFlags, motionEntry.metaState,  
                                                     motionEntry.buttonState,  
                                                     motionEntry.classification,  
                                                     dispatchEntry->transform,  
                                                     motionEntry.xPrecision, motionEntry.yPrecision,  
                                                     motionEntry.xCursorPosition,  
                                                     motionEntry.yCursorPosition,  
                                                     dispatchEntry->rawTransform,  
                                                     motionEntry.downTime, motionEntry.eventTime,  
                                                     motionEntry.pointerCount,  
                                                     motionEntry.pointerProperties, usingCoords);  
                break;  
            }  
        }  
        ... 
        
    }  
}

在这个方法中,则是从outboundQueue把所有的事件一条一条的取出来,解包成它要的类型,比如触摸事件就是MotionEntry,经过判断和一些处理之后,调用connection中的inputPublisherpublishMotionEvent方法,这里的inputPublisher我们之前分析创建InputChannel的时候有所了解,创建它所传的InputChannel为Server端的那个。 我们这里看一下它的publishMotionEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
status_t InputPublisher::publishMotionEvent(...) {  
    
    InputMessage msg;  
    msg.header.type = InputMessage::Type::MOTION;  
    msg.header.seq = seq;  
    msg.body.motion.eventId = eventId;  
    ...
    msg.body.motion.pointerCount = pointerCount;  
    for (uint32_t i = 0; i < pointerCount; i++) {  
        msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);  
        msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);  
    }  
  
    return mChannel->sendMessage(&msg);  
}

这里主要创建了InputMessage,将之前MotionEvent的所有参数放进去,通过ServerInputChannel调用sendMessage发送出去,sendMessage的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
status_t InputChannel::sendMessage(const InputMessage* msg) {  
    const size_t msgLength = msg->size();  
    InputMessage cleanMsg;  
    msg->getSanitizedCopy(&cleanMsg);  
    ssize_t nWrite;  
    do {  
        nWrite = ::send(getFd(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);  
    } while (nWrite == -1 && errno == EINTR);  

    return OK;  
}

我们知道,InputChannel内部的文件描述符为socket的标记,这里调用send方法,也就是通过socket把信息发送出去,这样的话,我们Client端的的socket也就会接收到,通过Looper,Client端的EventListener就可以接收到消息,我们的应用便可以接收到,到这里便把事件分发,完整的串起来了。

总结

以上就是事件在系统进程中的处理,包括它的事件获取,事件处理,最后通过socket发送,这样我们在客户端进程的InputChannel就能够接收到通知,客户端能够处理事件。配合我们之前分析过应用层的事件分发,到这里,不算事件的驱动相关的部分,事件分发的整个流程我们都有所了解了。

在这里事件从系统system_server通过Server的InputChannel发送的客户端的InputChannel,所采用的是unix的socket功能,而不是使用的binder或者其他的跨进程服务。这一块,结合我在网上查找的资料,以及我自己的想法,我想这里这样做的原因是,unix的sockt pair使用上很简单,并且运行效率很高效,不需要像binder一样涉及到进程和线程的切换。另外就是socket使用了fd,目标进程可以直接监听到事件的来临,而不是向binder一样需要有相应的接口涉及,可以更加实时的接收到事件,也不会因为binder线程阻塞而卡顿。

当然这是我的一些想法,也欢迎读者朋友说说你对于这块的想法。

看完评论一下吧

Android源码分析:从源头分析View事件的传递

2024-09-20 17:20:01

对于应用开发者的我们来说,经常会处理按钮点击,键盘输入等事件,而我们的处理一般都是在Activity中或者View中去做的。我们在上一篇文章中分析了View和Activity与Window的关系,其中的ViewRootImpl和我们的事件传递息息相关,上文未能分析,本文将对其进行分析。

事件介绍

事件是什么呢,广义上事件的发生可能在软件也可能在硬件层,在Android设备当中,我们会有可能有键盘触发,触摸触发,鼠标触发的各种事件。我们关注的通常有两种事件: 按键事件(KeyEvent): 这种色包括物理的按键,Home键,音量键,也包括软键盘触发的事件。 触摸事件(TouchEvent): 手指在屏幕上触摸触发的事件,可能是点击,也可能是拖动。

对于按键事件,一般有ACTION_DOWNACTION_UP两种状态,对于KeyEvent所支持的所有keyCode,我们都可以在KeyEvent当中找到。

而对于触摸事件来说,除了DOWNUP两种状态之外,还有ACTION_MOVEACTION_CANCEL等状态。

应用层的事件类图如下图所示:

classDiagram
class Parcelable {
<<interface>>
}
class InputEvent {
<<abstract>>
}
class KeyEvent
class MotionEvent

InputEvent<|--KeyEvent
InputEvent<|--MotionEvent
Parcelable<|..KeyEvent
Parcelable<|..MotionEvent
Parcelable<|..InputEvent

事件传递到View

我们一般处理View的onClick事件,而这个事件是在View的onTouchEvent中进行处理并执行的,在View中我们可以向上追溯到dispatchPointerEvent方法当中,这个方法就是外部向View传递事件的调用。我们知道Android的UI界面中的所有View是一个树形的结构,因此这些事件也就会通过dispatchTouchEvent一层一层的往下传,从而每一个View都能够接收到事件,并决定是否处理。

dispatchPointerEvent是在ViewRootImpl当中调用,代码如下:

1
2
3
4
5
6
7
8
9
private int processPointerEvent(QueuedInputEvent q) {  
    final MotionEvent event = (MotionEvent)q.mEvent;  
    ...
    boolean handled = mView.dispatchPointerEvent(event);  
    maybeUpdatePointerIcon(event);  
    maybeUpdateTooltip(event);  
    ...
    return handled ? FINISH_HANDLED : FORWARD;  
}

在Activity中,它的根视图为DecorViewViewRootImpl在执行它的dispatchPointerEvent方法,它再向下把触摸事件依次向下传递。

除了触摸事件,按键事件也是类似,ViewRootImpl当中会调用View的dispatchKeyEvent方法,View当中会做相应的处理或者向下传递。

ViewRootImpl中对事件的处理

对于ViewRootImpl当中是如何获取事件,并且向后传递的,我们这里以触摸事件为主进行分析,其他事件也类似。

ViewRootImpl中,定义写一些内部类,大概如下:

classDiagram
class InputStage {
<<abstract>>
+InputStage mNext;
+deliver(QueuedInputEvent q)
#finish(QueuedInputEvent q, boolean handled)
#forward(QueuedInputEvent q)
#onDeliverToNext(QueuedInputEvent q)
#onProcess(QueuedInputEvent q)
}
class AsyncInputStage {
<<abstract>>
#defer(QueuedInputEvent q)
}
InputStage <|-- AsyncInputStage
AsyncInputStage <|--NativePreImeInputStage
InputStage <|-- ViewPreImeInputStage
AsyncInputStage <|-- ImeInputStage
InputStage <|-- EarlyPostImeInputStage
AsyncInputStage <|-- NativePostImeInputStage
InputStage <|-- ViewPostImeInputStage
InputStage <|--SyntheticInputStage

上面这几个类就ViewRootImpl中处理事件的类,其初始化代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//ViewRootImpl.java
public void setView(...) {
	...
	CharSequence counterSuffix = attrs.getTitle();  
	mSyntheticInputStage = new SyntheticInputStage();  
	InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);  
	InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,  
        "aq:native-post-ime:" + counterSuffix);  
	InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);  
	InputStage imeStage = new ImeInputStage(earlyPostImeStage,  
        "aq:ime:" + counterSuffix);  
	InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);  
	InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,  
        "aq:native-pre-ime:" + counterSuffix);  
  
	mFirstInputStage = nativePreImeStage;  
	mFirstPostImeInputStage = earlyPostImeStage;
}

以上代码创建了多个InputStage,它们一起组成了输入事件处理的流水线。其中ViewPostImeInputStage中就会处理与触摸相关的事件,它的onProcess方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected int onProcess(QueuedInputEvent q) {  
    if (q.mEvent instanceof KeyEvent) {  
        return processKeyEvent(q);  
    } else {  
        final int source = q.mEvent.getSource();  
        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {  
            return processPointerEvent(q);  
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {  
            return processTrackballEvent(q);  
        } else {  
            return processGenericMotionEvent(q);  
        }  
    }  
}

可以看到,当我们的输入源为POINTER,触摸屏和鼠标的触发都是这一类。这个时候就会执行上面我们提到的 processPointerEvent方法,之后事件也就会传递到View当中。

这里我们知道了是通过InputStage的流水线拿到的事件,但是这个事件从何处来的呢,我们需要继续向上溯源。

ViewRootImpl从何处获得事件

关于这一点,我们仍然需要关注ViewRootImplsetView方法中的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//VieRootImpl.java
InputChannel inputChannel = null;  
if ((mWindowAttributes.inputFeatures  
        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {  
    inputChannel = new InputChannel();  
}
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,  
        getHostVisibility(), mDisplay.getDisplayId(), userId,  
        mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,  
        mTempControls, attachedFrame, compatScale);
...
if (inputChannel != null) {  
    
    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,  
            Looper.myLooper());  
  
}

在这里,我们创建了一个InputChannel,但是我们创建的InputChannel仅仅是java层的一个类,没法去获取到事件,随后我们调用WindowSessionaddToDisplayAsUser他就会获得mPtr,也就是Native层的InputChannel,具体内容随后再看相关代码。在15行,这里创建了一个WindowInputEventReceiver,它的参数为inputChannelLooper,这里一起看一下InputEventReceiver的构造方法,代码如下:

1
2
3
4
5
6
7
8
public InputEventReceiver(InputChannel inputChannel, Looper looper) {  
    mInputChannel = inputChannel;  
    mMessageQueue = looper.getQueue();  
    mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),  
            inputChannel, mMessageQueue);  
  
    mCloseGuard.open("InputEventReceiver.dispose");  
}

InputEventReceiver的初始化

这里主要是调用了nativeInit方法,并且获取到mReceivePtr,native的代码在android_view_InputEventReceiver.cpp当中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,  
        jobject inputChannelObj, jobject messageQueueObj) {  
    std::shared_ptr<InputChannel> inputChannel =  
            android_view_InputChannel_getInputChannel(env, inputChannelObj);  //获取Native成的InputChannel
    sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);  //获取native层的消息队列

    sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,  
            receiverWeak, inputChannel, messageQueue);  
    status_t status = receiver->initialize();  
    receiver->incStrong(gInputEventReceiverClassInfo.clazz); // 增加引用计数
    return reinterpret_cast<jlong>(receiver.get());  
}

在上面的代码中,先是分别获取了Native层的InputChannel和MessageQueue,之后创建了NativeInputEventReceiver,并且调用了它的initialize方法:

1
2
3
4
status_t NativeInputEventReceiver::initialize() {  
    setFdEvents(ALOOPER_EVENT_INPUT);  
    return OK;  
}

内部调用了setFdEvents方法,参数ALOOPER_EVENT_INPUT,这个参数表示监听文件描述符的读操作,其内部代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void NativeInputEventReceiver::setFdEvents(int events) {  
    if (mFdEvents != events) {  
        mFdEvents = events;  
        int fd = mInputConsumer.getChannel()->getFd();  
        if (events) {  
            mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);  
        } else {  
            mMessageQueue->getLooper()->removeFd(fd);  
        }  
    }  
}

这里就是拿到InputChannel的文件描述符,并且添加到Looper中去监听它的输入事件。我们暂时不会去阅读硬件层面的触发,以及事件如何发送到InputChannel当中,这里就大胆的假设,InputChannel当中有一个文件描述符,当有事件发生时候,会写入到这个文件当中去。而文件变化,Looper就会收到通知,事件也就发送出来了。

NativeInputEventReceiver 接收事件并分发

这个时候我们可以看一下NativeInputEventReceiver所实现的LooperCallbackhandleEvent方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {  

    constexpr int REMOVE_CALLBACK = 0;  
    constexpr int KEEP_CALLBACK = 1;  

    if (events & ALOOPER_EVENT_INPUT) {  
        JNIEnv* env = AndroidRuntime::getJNIEnv();  
        status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr);  
        mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");  
        return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK;  
    }  
    ... 
    return KEEP_CALLBACK;  
}

其中核心代码如上,就是判断如果事件为ALOOPER_EVENT_INPUT,则会调用consumeEvents方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,  
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {  
    ...
    ScopedLocalRef<jobject> receiverObj(env, nullptr);  
    bool skipCallbacks = false;  
    for (;;) {  
        uint32_t seq;  
        InputEvent* inputEvent;  
  
        status_t status = mInputConsumer.consume(&mInputEventFactory,  
                consumeBatches, frameTime, &seq, &inputEvent);  
        ...    
        assert(inputEvent);  
  
        if (!skipCallbacks) {  
            if (!receiverObj.get()) {  
                receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));  
                if (!receiverObj.get()) {  
                    ...
                    return DEAD_OBJECT;  
                }  
            }  
  
            jobject inputEventObj;  
            switch (inputEvent->getType()) {  
            case AINPUT_EVENT_TYPE_MOTION: {  
                MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);  
                if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {  
                    *outConsumedBatch = true;  
                }  
                inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);  
                break;  
            }  
            ...
            default:  
                assert(false); // InputConsumer should prevent this from ever happening  
                inputEventObj = nullptr;  
            }  
  
            if (inputEventObj) {  
                env->CallVoidMethod(receiverObj.get(),  
                        gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);  
                ...
                env->DeleteLocalRef(inputEventObj);  
            } else {  
                ...
            }  
        }  
    }  
}

上面的代码做过简化,switch的case只保留了一个。首先在第10行,我们看到这里调用了mInputConsumerconsume方法。这个InputConsumer是在Receiver创建的时候创建它,它用于到InputChannel中获取消息,并且按照类型包装成InputEvent的具体子类,并写入到inputEvent当中。在后面的Switch判断处,就可以根据它的类型做处理,从而封装成java类型的InputEvent。而receiverObj在第17行,通过jniGetReferent拿到java层的InputEventReceiver的引用,在41行调用了它的dispatchInputEvent方法,从而调用了java层的同名方法,代码如下:

1
2
3
4
private void dispatchInputEvent(int seq, InputEvent event) {  
    mSeqMap.put(event.getSequenceNumber(), seq);  
    onInputEvent(event);  
}

我们再到WindowInputEventReceiver中看onInputEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void onInputEvent(InputEvent event) {  
    List<InputEvent> processedEvents;  
    try {  
        processedEvents =  
            mInputCompatProcessor.processInputEventForCompatibility(event);  
    } finally {  
    }  
    if (processedEvents != null) {  
        if (processedEvents.isEmpty()) {  
            finishInputEvent(event, true);  
        } else {  
            for (int i = 0; i < processedEvents.size(); i++) {  
                enqueueInputEvent(  
                        processedEvents.get(i), this,  
                        QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);  
            }  
        }  
    } else {  
        enqueueInputEvent(event, this, 0, true);  
    }  
}

其中第4行代码,是为了兼容低版本设计的,只有应用的TargetSDKVersion小于23才会生效,这里我们就不关注它了。因此这里就只会执行第19行的代码,其内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void enqueueInputEvent(InputEvent event,  
        InputEventReceiver receiver, int flags, boolean processImmediately) {  
    QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);  
    if (event instanceof MotionEvent) {  
        MotionEvent me = (MotionEvent) event;  
    }    
    QueuedInputEvent last = mPendingInputEventTail;  
    if (last == null) {  
        mPendingInputEventHead = q;  
        mPendingInputEventTail = q;  
    } else {  
        last.mNext = q;  
        mPendingInputEventTail = q;  
    }  
    mPendingInputEventCount += 1;  
    if (processImmediately) {  
        doProcessInputEvents();  
    } else {  
        scheduleProcessInputEvents();  
    }  
}

这里的代码,把我们的Event包装成一个QueuedInputEvent,并且放置到mQueuedInputEventPool这个链表中,具体可以自行看obtainQueuedInputEvent方法。而根据我们之前传递的参数,可以看到这里后面会调用到doProcessInputEvents方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void doProcessInputEvents() {  
    // Deliver all pending input events in the queue.  
    while (mPendingInputEventHead != null) {  
        QueuedInputEvent q = mPendingInputEventHead;  
        mPendingInputEventHead = q.mNext;  
        if (mPendingInputEventHead == null) {  
            mPendingInputEventTail = null;  
        }  
        q.mNext = null;  
  
        mPendingInputEventCount -= 1;  	mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));  
  
        deliverInputEvent(q);  
    }  
	//除了我们收到调用来把事件队列的所有事件消费,还有一些消息本来是准备通过Handler发送消息来处理的,既然我们已经手动把所有消息都处理掉了,那么如果有等待处理的消息事件,也就不需要了,下面的代码就是把他们删掉
    if (mProcessInputEventsScheduled) {  
        mProcessInputEventsScheduled = false;  
        mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);  
    }  
}

这里的代码主要就是遍历之前的链表,把每一条消息都取出来,并且调用deliverInputEvent方法来把它分发掉,同时会把它从链表中删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void deliverInputEvent(QueuedInputEvent q) {  
    try {  
        if (mInputEventConsistencyVerifier != null) {  
            try {  //事件一致性检查,避免外面传过来应用无法处理的事件
                mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);  
            } finally {  
            }  
        }  
  
        InputStage stage;  //选择要使用的入口InputStage
        if (q.shouldSendToSynthesizer()) {  
            stage = mSyntheticInputStage;  
        } else {  
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;  
        }  
        ...
        if (stage != null) {  
            handleWindowFocusChanged();  
            stage.deliver(q);  //InputStage 开始分发事件
        } else {  
            finishInputEvent(q);  
        }  
    } finally {  

    }  
}

在这个方法中,主要就是根据事件的属性选择入口的InputStage,之后调用它的deliver方法,在这个方法中就会按照链式调用,最终能够处理掉的一个InputStage会将它处理,也就是把事件分发到应用中去。

到这里我们就完成了从InputChannel中获取事件,并且通过InputEventReceiver传递到Java层,并且通过InputStage转发到应用的View当中。

InputChannel的初始化

刚刚我们已经基本把事件处理在ViewRootImpl中的部分看完了,而我们在其中创建的InputChannel只是一个壳,想要看看它的真正的初始化,我们沿着之前调用的addToDisplayAsUser继续往后看。IWindowSession是一个AIDL定义的Binder服务,在它的定义中InputChannel使用了out进行修饰,表示它会被binder服务端修改,并写入数据。而这个addToDisplayAsUser方法内部最终会调用WMS的addWindow方法,其中和InputChannel相关代码如下:

1
2
3
4
5
6
7
8
final WindowState win = new WindowState(this, session, client, token, parentWindow,  
        appOp[0], attrs, viewVisibility, session.mUid, userId,  
        session.mCanAddInternalSystemWindow);
        final boolean openInputChannels = (outInputChannel != null  
        && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);  
if  (openInputChannels) {  
    win.openInputChannel(outInputChannel);  
}

这里outInputChannel就是我们从客户端传过来的那个InputChannel的壳,随后便调用了WindowStateopenInputChannel方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void openInputChannel(InputChannel outInputChannel) {   
    String name = getName();   //获取window的name
    mInputChannel = mWmService.mInputManager.createInputChannel(name);  //创建
    mInputChannelToken = mInputChannel.getToken();  
    mInputWindowHandle.setToken(mInputChannelToken);  
    mWmService.mInputToWindowMap.put(mInputChannelToken, this);  
    if (outInputChannel != null) {  
        mInputChannel.copyTo(outInputChannel);  //将Native Channel写入我们传入的InputChannel
    } else {  
    }  
}

这里就是调用InputManager去创建InputChannel,并且把它和我们的WIndow关联,以及保存到我们传入的InputChannel当中,这样我们的View层面就可以通过InputChannel获取到事件了。InputManagerService创建InputChannel的部分这里就不讨论了,留待以后讨论。

总结

到此为止,就分析完了应用侧从WMS到View,如何初始化InputEventReceiver,InputEventReceiver和InputChannel关联起来,事件如何从InputChannel一直传递到我们的View的了。

sequenceDiagram
InputChannel-->>NativeInputEventReceiver: handleEvent
note right of InputChannel: notify has event via Looper 
NativeInputEventReceiver->> NativeInputEventReceiver: consumeEvents
NativeInputEventReceiver->>+ InputChannel: consume
note right of InputChannel: get event from InputChannel
InputChannel-->>-NativeInputEventReceiver: return inputEvent
NativeInputEventReceiver->>InputEventReceiver: dispatchInputEvent
InputEventReceiver->>InputEventReceiver: onInputEvent
InputEventReceiver->>ViewRootImpl: enqueueInputEvent
ViewRootImpl->>ViewRootImpl: doProcessInputEvents
ViewRootImpl->>ViewRootImpl: deliverInputEvent
ViewRootImpl->>+ViewPostImeInputStage: deliver
ViewPostImeInputStage->>ViewPostImeInputStage:onProcess
ViewPostImeInputStage->>ViewPostImeInputStage: processPointerEvent
ViewPostImeInputStage-->>+View: dispatchPointerEvent
View->>View:dispatchTouchEvent
View->>View: onTouch
View-->>-ViewPostImeInputStage: return is consume it or not
ViewPostImeInputStage-->>-ViewRootImpl: finish deliver

之前的分析涉及到了InputChannel的初始化和InputEventReceiver的初始化,直接看可以会比较绕人,上面从事件分发角度画了一下事件从InputChannel一直流转到View的一个时序图,希望对于你理解这个流程有所理解。如果哪里存在疏漏,也欢迎读者朋友们评论指点。

看完评论一下吧

Android源码分析:Window与Activity与View的关联

2024-09-19 21:30:25

Activity是四大组件中和UI相关的那个,应用开发过程中,我们所有的界面基本都需要使用Activity才能去渲染和绘制UI,即使是ReactNative,Flutter这种跨平台的方案,在Android中,也需要一个Activity来承载。但是我们的Activity内我们设置的View又是怎么渲染到屏幕上的呢,这背后又有WindowManager和SurfaceFlinger来进行工作。本文就来看看WindowManger如何管理Window,以及Window如何与Activity产生关系的呢。

Activity与Window的初见

Activity的创建是在ActivityThreadperformLaunchActivity中,这里会创建要启动的Activity,并且会调用Activity的attach方法,在这个方法当中就会创建Window,其中和Window相关的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mWindow = new PhoneWindow(this, window, activityConfigCallback);  
mWindow.setWindowControllerCallback(mWindowControllerCallback);  
mWindow.setCallback(this);  
mWindow.setOnWindowDismissedCallback(this);  
mWindow.getLayoutInflater().setPrivateFactory(this);  
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {  
    mWindow.setSoftInputMode(info.softInputMode);  
}  
if (info.uiOptions != 0) {  
    mWindow.setUiOptions(info.uiOptions);  
}
mWindow.setWindowManager(  
        (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),  
        mToken, mComponent.flattenToString(),  
        (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);  
if (mParent != null) {  
    mWindow.setContainer(mParent.getWindow());  
}  
mWindowManager = mWindow.getWindowManager();

这里我们可以看到为Activity创建了Window,目前Android上面的Window实例为PhoneWindow,同时还给Window设置了WindowManager,不过这里的WindowManager仅仅是一个本地服务,它的实现为WindowManagerImpl,它的注册代码在SystemServiceRegister.java中,代码如下:

1
2
3
4
5
6
registerService(Context.WINDOW_SERVICE, WindowManager.class,  
        new CachedServiceFetcher<WindowManager>() {  
    @Override  
    public WindowManager createService(ContextImpl ctx) {  
        return new WindowManagerImpl(ctx);  
    }});

而我们这个WindowManagerImpl内部持有持有了一个WindowManagerGlobal,看名字就知道它应该会涉及到跨进程通讯,去看它代码就知道它内部有两个成员,分别是sWindowManagerServicesWindowSession,这两个成员就用于跨进程通讯。这里我们先知道有这几个类,后面到用处再继续分析。

--- 
title: WindowManager相关类图
---
classDiagram
directioni TB
class ViewManager {
 <<interface>>
 addView(view, params)
 updateViewLayout(view, params)
 removeView(view)
}
class WindowManager {
<<interface>>
}
class WindowManagerImpl {
- WindowManagerGlobal mGlobal;
- IBinder mWindowContextToken;
- IBinder mDefaultToken;
}
ViewManager <|-- WindowManager
WindowManager <|.. WindowManagerImpl
class WindowManagerGlobal {
IwindowManager sWindowManagerService;
IWindowSession SwindowSession;
}
 WindowManagerImpl ..> WindowManagerGlobal
 class Window {
 <<abstract>>
 WindowManager mWindowManager;
 }
 Window <|.. PhoneWindow
Window ..> WindowManager
 class IWindow {
<<Interface>>
}
IWindow <|--W
class ViewRootImpl {
W mWindow
IWindowSession mWindowSession
}
ViewRootImpl .. W
ViewRootImpl .. IWindowSession
IWindowSession .. W

这里只可出了App进程相关的一些类,System_Server相关未列出,后面涉及到相关部分的时候再进行分析。

Window与View的邂逅

我们一般情况下会在Activity的onCreate当中去调用setContentView,只有这样我们的View才能够显示出来。因此我们直接看这个方法的调用:

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {  
    getWindow().setContentView(layoutResID);  
    initWindowDecorActionBar();  
}

其中就是调用了window的同名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void setContentView(int layoutResID) {  
    if (mContentParent == null) {  
        installDecor();  
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  
        mContentParent.removeAllViews();  
    }  
  
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {  
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,  
                getContext());  
        transitionTo(newScene);  //执行页面Transition动画
    } else {  
        mLayoutInflater.inflate(layoutResID, mContentParent);  
    }  
    mContentParent.requestApplyInsets();  
    final Callback cb = getCallback();  
    if (cb != null && !isDestroyed()) {  
        cb.onContentChanged();  
    }  
    mContentParentExplicitlySet = true;  
}

这里我们主要是将我们的ContentView添加到mContentParent当中去,这个mContentParent有可能为空,需要我们通过installDecor来创建,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void installDecor() {  
    mForceDecorInstall = false;  
    if (mDecor == null) {  
        mDecor = generateDecor(-1);  
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  
        mDecor.setIsRootNamespace(true);  
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {  
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);  
        }  
    } else {  
        mDecor.setWindow(this);  
    }  
    if (mContentParent == null) {  
        mContentParent = generateLayout(mDecor);  
  
        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.  
        mDecor.makeFrameworkOptionalFitsSystemWindows();  
  
        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(  
                R.id.decor_content_parent);  
  
        if (decorContentParent != null) {  
            mDecorContentParent = decorContentParent;  
            mDecorContentParent.setWindowCallback(getCallback());  
            if (mDecorContentParent.getTitle() == null) {  
                mDecorContentParent.setWindowTitle(mTitle);  
            }  
  
            final int localFeatures = getLocalFeatures();  
            for (int i = 0; i < FEATURE_MAX; i++) {  
                if ((localFeatures & (1 << i)) != 0) {  
                    mDecorContentParent.initFeature(i);  
                }  
            }  
  
            mDecorContentParent.setUiOptions(mUiOptions);  
  
            ...
  
             PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);  
            if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {  
                invalidatePanelMenu(FEATURE_ACTION_BAR);  
            }  
        } else {  
            mTitleView = findViewById(R.id.title);  
            if (mTitleView != null) {  
                //title view的设置
            }  
        }  
  
        if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {  
            mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);  
        }  
  
        if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {  
            ...
            //页面动画的读取和设置    
        }  
    }  
}

这里我们可以看到主要做的就是创建了decorView和ContentParent,还有一些动画,标题之类的初始化我们这里就跳过了。DecorView就是App Activity页面最底层的容器,它为我们封装了状态栏,底部导航栏,App页面的内容的展示。而ContentParent的初始化,则是根据Activity的设置,根据是否展示状态栏,是否展示标题栏等,进行加载相应的布局,加载到DecorView当中,最后com.android.internal.R.id.content对应的FrameLayout就会成为ContentParent。 当这一切做完,我们的页面View就成功的添加到Window当中了,但是它是如何展示出来的呢,还需要继续往后看。我们需要前往ActivityThread的handleResumeActivity方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//调用Activity的onResume方法
if (!performResumeActivity(r, finalStateRequest, reason)) {  
    return;  
}
//r为ActivityClientRecord
final Activity a = r.activity;
//检查当前的Activity是否能显示
boolean willBeVisible = !a.mStartedActivity;  
if (!willBeVisible) {  
    willBeVisible = ActivityClient.getInstance().willActivityBeVisible(  
            a.getActivityToken());  
}  
if (r.window == null && !a.mFinished && willBeVisible) {  
    r.window = r.activity.getWindow();  //把activity的window保存到r.window中
    View decor = r.window.getDecorView();  
    decor.setVisibility(View.INVISIBLE);  
    ViewManager wm = a.getWindowManager();  
    WindowManager.LayoutParams l = r.window.getAttributes();  
    a.mDecor = decor;  
    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;  
    l.softInputMode |= forwardBit;  
    if (r.mPreserveWindow) {  
        a.mWindowAdded = true;  
        r.mPreserveWindow = false;  
        ViewRootImpl impl = decor.getViewRootImpl();  
        if (impl != null) {  
            impl.notifyChildRebuilt();  
        }  
    }  
    if (a.mVisibleFromClient) {  
        if (!a.mWindowAdded) {  
            a.mWindowAdded = true;  
            wm.addView(decor, l);  //调用windowManager添加decorView
        } else {  
            a.onWindowAttributesChanged(l); 
        }  
    }  
} else if (!willBeVisible) {  
    r.hideForNow = true;  
}

可以看到上面的代码把window保存到了ActivityClientRecord当中,同时调用了WindowManager的addView方法,去添加view。我们继续往后看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {  
    ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();  
    WindowManager.LayoutParams l = impl != null  
            ? impl.mWindowAttributes : r.window.getAttributes();  
    if ((l.softInputMode  
            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)  
            != forwardBit) {  
        l.softInputMode = (l.softInputMode  
                & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))  
                | forwardBit;  
        if (r.activity.mVisibleFromClient) {  
            ViewManager wm = a.getWindowManager();  
            View decor = r.window.getDecorView();  
            wm.updateViewLayout(decor, l);  
        }  
    }  
  
    r.activity.mVisibleFromServer = true;  
    mNumVisibleActivities++;  
    if (r.activity.mVisibleFromClient) {  
        r.activity.makeVisible();  
    }  
  
    if (shouldSendCompatFakeFocus) {  
        if (impl != null) {  
            impl.dispatchCompatFakeFocus();  
        } else {  
            r.window.getDecorView().fakeFocusAfterAttachingToWindow();  
        }  
    }  
}

上面的代码中,我们看到主要做了两件事情,一个是调用updateViewLayout去更新视图的属性,但是updateViewLayout也要属性发生变化,并且有输入法的时候才会执行,另外就是调用activity的makeVisible方法去展示View。

这个过程我们需要分析如下两步。

  1. 调用addView添加decorView
  2. 调用activity.makeVisible来显示 我们分别看一下这两个方法的实现

WMS与ViewRootImpl的遇见:调用WindowManger的addView

1
2
3
4
5
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {  
    applyTokens(params);  
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,  
            mContext.getUserId());  
}

这里就是调用mGlobaladdView方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void addView(View view, ViewGroup.LayoutParams params,  
        Display display, Window parentWindow, int userId) {  

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;  
    ....
  
    ViewRootImpl root;  
    View panelParentView = null;  
  
    synchronized (mLock) {  
  
        int index = findViewLocked(view, false);  
        ...
  
        if (windowlessSession == null) {  
            root = new ViewRootImpl(view.getContext(), display);  
        } else {  
            root = new ViewRootImpl(view.getContext(), display,  
                    windowlessSession, new WindowlessWindowLayout());  
        }  
  
        view.setLayoutParams(wparams);  
  
        mViews.add(view);  
        mRoots.add(root);  
        mParams.add(wparams);  

        try {  
            root.setView(view, wparams, panelParentView, userId);  
        } catch (RuntimeException e) {  
            final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);  
            // BadTokenException or InvalidDisplayException, clean up.  
            if (viewIndex >= 0) {  
                removeViewLocked(viewIndex, true);  
            }  
            throw e;  
        }  
    }  
}

在正常的App页面,windowlessSession会一直为空,这里就会创建一个ViewRootImpl,并且把我们的DecorView以及WindowParams都传进去。并且viewrootwparams都会按照顺序存到List当中。这里我们需要去看ViewRootImpl的setView方法,其中和添加到屏幕相关的代码如下:

1
2
3
4
5
6
7
requestLayout(); //测量布局
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,  
        getHostVisibility(), mDisplay.getDisplayId(), userId,  
        mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,  
        mTempControls, attachedFrame, compatScale);
...
view.assignParent(this); //将ViewRootImpl设置为DecorView的parent

这里的mDisplay为外面从Context中所获取的,用于指定当前的UI要显示到哪一个显示器上去。这里的mWindowSession的获取代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowManagerGlobal.java
@UnsupportedAppUsage  
public static IWindowSession getWindowSession() {  
    synchronized (WindowManagerGlobal.class) {  
        if (sWindowSession == null) {  
            try {  
                @UnsupportedAppUsage  
	InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();  
                IWindowManager windowManager = getWindowManagerService();  
                sWindowSession = windowManager.openSession(  
                        new IWindowSessionCallback.Stub() {  
                            @Override  
                            public void onAnimatorScaleChanged(float scale) {  
                                ValueAnimator.setDurationScale(scale);  
                            }  
                        });  
            } catch (RemoteException e) {  
                throw e.rethrowFromSystemServer();  
            }  
        }  
        return sWindowSession;  
    }  
}

可以看到就是通过IWindowManger这个Binder服务调用了openSession来获取了一个WindowSession。其代码如下:

1
2
3
4
//WindowManagerService.java
public IWindowSession openSession(IWindowSessionCallback callback) {  
    return new Session(this, callback);  
}

在System_Server端,创建了一个Session对象来提供相关的服务。它的addToDisplayAsUser方法又调用了WMSaddWindow方法,这个方法比较长我们只看其中和UI展示相关的部分,并且UI类型不是App的普通UI的也都给省略掉。

1
2
3
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,  
        appOp);
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);

第一行代码首先是去检查我们当前要展示的view,它的类型是否支持去展示。第3行代码的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private DisplayContent getDisplayContentOrCreate(int displayId, IBinder token) {  
    if (token != null) {  
        final WindowToken wToken = mRoot.getWindowToken(token);  
        if (wToken != null) {  
            return wToken.getDisplayContent();  
        }  
    }  
  
    return mRoot.getDisplayContentOrCreate(displayId);  
}

mRoot为一个RootWindowContainer对象,之前我们在分析Activity的启动过程中已经见到了它,我们的ActivityRecord和Task都存在它当中。这里wToken初始情况一般为null因此会执行下面的getDisplayContentOrCreate方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DisplayContent getDisplayContentOrCreate(int displayId) {  
    DisplayContent displayContent = getDisplayContent(displayId);  
    if (displayContent != null) {  
        return displayContent;  
    }  
    ...
    final Display display = mDisplayManager.getDisplay(displayId);  
    ... 
    displayContent = new DisplayContent(display, this);  
    addChild(displayContent, POSITION_BOTTOM);  
    return displayContent;  
}

这里就是根据displayId从列表中去拿DisplayContent如果不存在就去创建一个并且保存到列表中,方便下次使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//WindowManagerService.addWindow
WindowToken token = displayContent.getWindowToken(  
        hasParent ? parentWindow.mAttrs.token : attrs.token);
if (token == null) {
{  
	...
    final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();  
    token = new WindowToken.Builder(this, binder, type)  
            .setDisplayContent(displayContent)  
                      .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)  
            .setRoundedCornerOverlay(isRoundedCornerOverlay)  
            .build();  
}

继续看addWindow的内容,如果displayContent是新创建的,那么这里拿到的token就会为空,因此这里调用了client.asBinder来获取IBinder,或者直接拿’attr’中的token,这个client为IWindow类型,它在应用侧为W的实例,它是ViewRootImpl的一个内部类。这里创建完WindowToken之后,我们可以继续往后看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//WindowManagerService.addWindow
final WindowState win = new WindowState(this, session, client, token, parentWindow,  
        appOp[0], attrs, viewVisibility, session.mUid, userId,  
        session.mCanAddInternalSystemWindow);
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();  
displayPolicy.adjustWindowParamsLw(win, win.mAttrs);  
attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);  
attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid,  
        callingPid);  
win.setRequestedVisibilities(requestedVisibilities);  
  
res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);

这里创建的WindowState用于保存Window的状态,可以说是Window在WMS中存储的一个章台。随后从DisplayContent中拿到了DisplayPolicy这个类主要是用于控制显示的一些行为,比如状态栏,导航栏的显示状态之类的。这里会根据WindowParams来调整DisplayPolicy的参数,以及调用validateAddingWindowLw检查当前的window是否能够添加的系统界面中,这个app普通type不涉及。

1
2
3
4
//WindowManagerService.addWindow
win.attach();  
mWindowMap.put(client.asBinder(), win);  
win.initAppOpsState();

win.attach方法如下:

1
2
3
void attach() {  
    mSession.windowAddedLocked();  
}

其中就调用了SessionwindowAddedLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void windowAddedLocked() {  
    if (mPackageName == null) {  
        final WindowProcessController wpc = mService.mAtmService.mProcessMap.getProcess(mPid);  
        if (wpc != null) {  
            mPackageName = wpc.mInfo.packageName;  
            mRelayoutTag = "relayoutWindow: " + mPackageName;  
        } else {  
        }  
    }  
    if (mSurfaceSession == null) {  
        mSurfaceSession = new SurfaceSession();  
        mService.mSessions.add(this);  
        if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {  
            mService.dispatchNewAnimatorScaleLocked(this);  
        }  
    }  
    mNumWindow++;  
}

对于每个进程第一次使用openSession创建的Session这个地方都会执行,主要就是创建了SurfaceSession,并且保存到WMSmSessions当中去。之后又把client作为key,WindowState为value存放到mWindowMap当中。

1
2
3
//WindowManagerService.addWindow
win.mToken.addWindow(win);
displayPolicy.addWindowLw(win, attrs);

先看这个WindowToken.addWindow方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void addWindow(final WindowState win) {  
   if (mSurfaceControl == null) {  
        createSurfaceControl(true /* force */);  

        reassignLayer(getSyncTransaction());  
    }  
    if (!mChildren.contains(win)) {  
        addChild(win, mWindowComparator);  
        mWmService.mWindowsChanged = true;  
    }  
}

这里创建了一个SurfaceControl,并且保存到了WindowList当中去。随后再看displayyPolicy.addWindowLw,其中主要用于处理inset相关的处理,这里也先跳过。到此位置addView的代码基本就看完了。

调用activity.makeVisible来显示

1
2
3
4
5
6
7
8
void makeVisible() {  
    if (!mWindowAdded) {  
        ViewManager wm = getWindowManager();  
        wm.addView(mDecor, getWindow().getAttributes());  
        mWindowAdded = true;  
    }  
    mDecor.setVisibility(View.VISIBLE);  
}

我们之前已经分析过addView了,这里mWindowAdded也是为true,这里的addView因此是不会被执行的。我们看一下下面的setVisibility,这个就是我们的普通View的方法,还是直接看源码:

1
2
3
4
//View.java
public void setVisibility(@Visibility int visibility) {  
    setFlags(visibility, VISIBILITY_MASK);  
}

这里是直接调用了setFlags方法,其中和设置显示相关的部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
final int newVisibility = flags & VISIBILITY_MASK;  
if (newVisibility == VISIBLE) {  
    if ((changed & VISIBILITY_MASK) != 0) {  
        mPrivateFlags |= PFLAG_DRAWN;  
        invalidate(true);  
  
        needGlobalAttributesUpdate(true);  
        shouldNotifyFocusableAvailable = hasSize();  
    }  
}

if ((changed & VISIBILITY_MASK) != 0) {  
    if (mParent instanceof ViewGroup) {  
        ViewGroup parent = (ViewGroup) mParent;  
        parent.onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),  
                newVisibility);  
        parent.invalidate(true);  
    } else if (mParent != null) { 
        mParent.invalidateChild(this, null);  
    }
}

DecorView的parent为ViewRootImpl,因此上面会调用ViewRootImplinvalidateChild方法,内部会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {  
    checkThread();  
  
    if (dirty == null) {  
        invalidate();  
        return null;  
    } else if (dirty.isEmpty() && !mIsAnimating) {  
        return null;  
    }  
  
    if (mCurScrollY != 0 || mTranslator != null) {  
        mTempRect.set(dirty);  
        dirty = mTempRect;  
        if (mCurScrollY != 0) {  
            dirty.offset(0, -mCurScrollY);  
        }  
        if (mTranslator != null) {  
            mTranslator.translateRectInAppWindowToScreen(dirty);  
        }  
        if (mAttachInfo.mScalingRequired) {  
            dirty.inset(-1, -1);  
        }  
    }  
  
    invalidateRectOnScreen(dirty);  
  
    return null;  
}

这段代码会检查需要从新绘制的区域,并且放在dirty当中,最后调用invalidateRectOnScreen方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void invalidateRectOnScreen(Rect dirty) {  
    final Rect localDirty = mDirty;  
  
    // Add the new dirty rect to the current one  
    localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);  
    final float appScale = mAttachInfo.mApplicationScale;  
    final boolean intersected = localDirty.intersect(0, 0,  
            (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));  
    if (!intersected) {  
        localDirty.setEmpty();  
    }  
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {  
        scheduleTraversals();  
    }  
}

这里仍然检查dirty区域,并且去做Traversal。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void scheduleTraversals() {  
    if (!mTraversalScheduled) {  
        mTraversalScheduled = true;  
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();  
        mChoreographer.postCallback(  
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  
        notifyRendererOfFramePending();  
        pokeDrawLockIfNeeded();  
    }  
}

这里就是启动线程去不断的页面的刷新重绘,就不分析了。最终会执行到performTraversals方法,其中有如下代码我们比较关注:

1
2
3
4
if (mFirst || windowShouldResize || viewVisibilityChanged || params != null  
        || mForceNextWindowRelayout) {
	relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
}

当首次执行这个方法的时候mFirst为true,除了这个条件之外,window需要从新计算size,view的可见性变化,windowParams变化等任一条件满足就会执行这里。我们在继续看里面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (relayoutAsync) {  
    mWindowSession.relayoutAsync(mWindow, params,  
            requestedWidth, requestedHeight, viewVisibility,  
            insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,  
            mLastSyncSeqId);  
} else {  
    relayoutResult = mWindowSession.relayout(mWindow, params,  
            requestedWidth, requestedHeight, viewVisibility,  
            insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,  
            mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl,  
            mTempInsets, mTempControls, mRelayoutBundle);  
        ...
}

当view为本地进行Layout且一些其他的条件符合,并且它的位置大小没有变化的时候,才会是relayoutAsync,不过两个最终的在服务端都会调用relayout方法,区别就是这里relayout的时候传过去了一个mSurfaceControl,这个接口是AIDL定义的,这个参数定义的为out,服务端会传输值到这个对象里,我们随后会看到,因为非异步是大多数情况的调用,这里也对他进行分析。在Session的relayout方法中调用了如下代码:

1
2
3
4
int res = mService.relayoutWindow(this, window, attrs,  
        requestedWidth, requestedHeight, viewFlags, flags, seq,  
        lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,  
        outActiveControls, outSyncSeqIdBundle);

这里就是调用了WMSrelayoutWindow方法,其中我们关注的有一下代码:

1
2
3
4
5
6
7
8
9
final WindowState win = windowForClientLocked(session, client, false);
if (shouldRelayout && outSurfaceControl != null) {  
    try {  
        result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);  
    } catch (Exception e) {  
        ...
        return 0;  
    }  
}

为应用提供画布容器

这里看一下这个createSurfaceControl的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
WindowSurfaceController surfaceController;  
try {  
    surfaceController = winAnimator.createSurfaceLocked();  
} finally {  

}  
if (surfaceController != null) {  
    surfaceController.getSurfaceControl(outSurfaceControl);  
  
} else {  
    outSurfaceControl.release();  
}

第三行主要是创建一个WindowSurfaceController对象,第8行则是使用这个对象去获取SurfaceControl,我们看一下它的代码:

1
2
3
void getSurfaceControl(SurfaceControl outSurfaceControl) {  
    outSurfaceControl.copyFrom(mSurfaceControl, "WindowSurfaceController.getSurfaceControl");  
}

SurfaceControlcopyFrom方法代码如下:

1
2
3
4
5
6
7
public void copyFrom(@NonNull SurfaceControl other, String callsite) {  
    mName = other.mName;  
    mWidth = other.mWidth;  
    mHeight = other.mHeight;  
    mLocalOwnerView = other.mLocalOwnerView;  
    assignNativeObject(nativeCopyFromSurfaceControl(other.mNativeObject), callsite);  
}

最主要的是最后的assignNativeObject赋值到我们从app进程传过来的SurfaceControl当中。native层的SurfaceControl有如下几个成员变量:

1
2
3
4
5
6
sp<SurfaceComposerClient>   mClient;  
sp<IBinder>                 mHandle;  
sp<IGraphicBufferProducer>  mGraphicBufferProducer;  
mutable Mutex               mLock;  
mutable sp<Surface>         mSurfaceData;  
mutable sp<BLASTBufferQueue> mBbq;

其中就有Surface,而我们在服务端拿到的这个SurfaceControl随后会写回客户端,这样App进程就可以把UI元素绘制到这个Surface上面了。

前面有列过客户端WindowManager相关的类,这里在列一下system_server进程中相关的类:

classDiagram
class IWindowManager {
<<interface>>
}
class Stub["IWindowManager.Stub"]
IWindowManager <|..Stub
class WindowManagerService {
WindowManagerPolicy mPolicy
ArraySet~Session~ mSessions
HashMap~IBinder, WindowState~ mWindowMap
RootWindowContainer mRoot
}
Stub <|--WindowManagerService

class IWindowSession {
<<Interface>>
}
class SessionStub["IWindowSession.Stub"] {
<<abstract>>
}
class Session {
WindowManagerService mService
}
IWindowSession <|..SessionStub
SessionStub<|--Session
WindowContainer<|--RootWindowContainer
WindowContainer <|-- WindowToken
WindowContainer <|--WindowState
WindowToken .. WindowManagerService
Session .. WindowManagerService
RootWindowContainer .. WindowManagerService

总结

我们在调用WMS的addWindow的时候,并没有把View直接传过来,所传过来的WindowLayoutParams当中,宽和高是比较重要的信息,因为在对调用这个方法之前,代码中先是执行了requestLayout去测量的布局的尺寸,并且在返回参数中通过Rect返回了画布的尺寸。我们也知道通过SurfaceControl为我们提供了Surface,这样客户端就能够把UI数据写上去了。而这样,这个Window与View就能够与系统的其他服务一起,把我们的UI显示到屏幕上了。

在与WMS初始通信的时候,WMS服务端为App创建了Session这个对象,App通过这个对象来与服务端进行Binder通讯。同时,App进程在创建ViewRootImpl的时候创建了W这个对象,它是IWindow的binder对象,服务端可以通过这个对象来与app进程通讯。为了方便理解,关于服务端和客户端,我又画了如下图,希望对你理解它们有所帮助。

以上就应用的窗口与Activity相关的分析,整体流程还是比较复杂的,如果哪里存在疏漏,也欢迎读者朋友们评论指点。另外关于应用的事件分发也会涉及到WMS和ViewRootImpl,为了使得文章不至于太长,就留到下次再进行分析。

看完评论一下吧

回皖北农村观察随记

2024-09-18 22:05:48

老家在皖北农村,一年回不了几次,上次过年回去的,这次中秋想着回家看看老爹,就回去看了看。正好也记录一下回去的所看所感。

中秋节的节味是越来越淡了,因为大部分的人都在外打工,现在这个节点跟过年比农村依然很冷清。但是跟平时比还人还要多一点点,因为有些住在县城的人,中秋这天还跑回来过节了。

现在大部分人家里的地都承包出去了,还能干活的人都出去打工了。留下来的只剩老弱病,以及带着小孩上学的部分发家庭。

农村的教育

我们村里的小学已经关闭好多年了,附近几个村的基本也都关的差不多了。我以前上的镇子上的初中,以前人多的时候一个年级差不多上千人,现在在那边上学的小妹说,一个年级也就200多人。我想原因是有两方面,一是不少像我这样的,在外地生活的,家里小孩也就不在当地就学了。另一方面是,县城的教育扩张,县城内新建了不少学校,下面农村的居民只要购房就可以在县城上学,不少亲戚家的小孩就为了上学在县城买房了,如此带动了县城的房市,也使得农村的教育更加的衰败。

环境

村子里面的水沟,原先都是互相联通,最后可以连接到更大的河流。而最近十几年的农村建房填沟,导致了水面的面积越来越小,也不能互相联通。叠加上之前的各种乱扔垃圾,现在大部分的水面都飘满了浮萍,浮萍之下也都是黑水。

不少养殖户的污水,以及各种排泄物直接流入水沟,缺少监督和管理。这也是导致水体污染的一打原因,而前几年政府所规划建设的污水处理在附近几个村子还没有能够见到。

当然也有在变好的方面,目前各个村子都放置了垃圾桶,定期有垃圾车过来统一收走垃圾。政府补贴给每个村子找了个村民来打扫各村子的垃圾,每个月能拿到一点钱,这样一来村子的各条路边也都鲜有垃圾踪影。

经济

排除出去打工的人,留在村里的人想要挣点钱还是挺难的。大部分的土地,目前是一小部分人承包的,这样其他人没有土地的束缚都能安心在外工作。而留下大规模承包土地,一方面可以大规模种植提高收益,另外还有部分政府补贴。

在田地里走了一圈,而且在附近乡镇也看过,大部分的农田都种植了玉米。这几年大部分的土地都是这样,秋季种植小麦,收完小麦种植玉米,这两种都很方便机械收割。而今年,由于夏季下雨较少,玉米的收成不是很好,玉米长得不够饱满,而已经获悉的玉米收购价格也不是很好。部分村民已经准备把玉米卖去做青储,收购商会把玉米连秸秆一起收走,免去农民的许多麻烦。

除了种植,部分家里有空间的村民还会选择在家养殖,目前我们村养羊的最多。目前羊都是圈养的,会选择到附近割草喂养,最近县城有草坪种植者会定期割草,于是大家便前往拉草喂养。如此一来,即帮忙处理了割下来的草,又不用找地方割草了。但是大家的羊养的却又不太好,以我父亲养的为例,生下来的小羊羔死了不少,再加上大羊也因为生病死了一两只,导致养了一年多,花钱买了不少羊,但是却没几只羊能卖出去。而想要考养殖挣到点钱,还是挺难的,饲料,买羊,给羊治病的各种花费都不少。

而剩下还有几个为了小孩的教育留在农村的青壮年呢,一般都会想点办法,到县城或者在乡镇上做点生意。我的表哥一家选择了在乡镇上面卖水果,表面上看是在家陪伴小孩,但是每天在摊位上忙碌,又有多少时间能够陪伴小孩呢。

基础设施

由于农村的居住点分散,农村的基础设置建设天然比城市要差很多。不过这几年,我们这里的基础设施倒是有了一些提升。电力和通信这两块已经很多年很稳了,光纤也都通到了各个村子。

前两年,我们村也通了自来水。但是时至今日,很多人还是宁愿自己抽地下水而不愿意使用自来水,许多人还是对自来水不够信任。自来水的水质和稳定性还不够让人信任。但是目前抽取的地下水也越来越差,不过滤也难以直接使用,不少人选择去乡镇购买按量计费的直饮水。还是希望当地水厂能够提高水质和服务,以免大家不用,进而导致水厂无法提供稳定服务。

最近呢,村里也通上了燃气管道,这样看起来大家就可以使用天然气了。但是不同于城市的商品房都预装了天然气入户管道,农村需要各家接入时候铺设入户管道。据介绍安装天然气大约需要两千三百元,大部分人都被这个价格所劝退。对此我觉得,提高初期的燃气价格反倒是比一次性收这么多安装费更容易让人接受。

我家门口的道路是县道,是十来年前修的柏油路,目前是有城乡公交经过。然而之前经常有超载货车经过,再加上养护不好,目前路面已经坑坑洼洼,很多路段破损不堪。这只能归因于贫穷,经济落后,县里没有足够的资金维护道路。

新农村

隔壁的村里,靠近县道的部分居然搞起了新农村。具体搞了什么呢,就是把靠近路边的房子全都刷白了,路边的东西都清理掉了,还种了树,建了围栏。

另外还建了小公园,这公园是真的小,村民说,这个就是浪费钱,在这公园里转还不如去农田里面转一圈呢。

也有好的地方,就是新建了卫生所,大家看个小病会更加方便。

新农村,我想,出发点还是好的,但是实践的太过粗糙,太过注重表面工程。一个是建设的下水道之类的设施太过劣质,窨井盖还没用多久已经烂掉了。另外就是并没有考虑村民的使用需求,没有 把钱花到刀刃上。

总结

皖北的农村,可能跟很多的农村也类似,村里的人越来越少。基础设置也在慢慢的建设,但是人居环境仍然有很大提高的空间。而相比之下,之前去过的浙江农村环境则是要好很多,我们这里还有很大的提升空间,作为在外的游子,也希望家乡能够越来越好。

看完评论一下吧

Android源码分析:Activity启动流程Task相关分析

2024-09-13 17:57:05

Activity的启动分析,很大一块需要了解的是Activity的Task管理,以及启动过程中Task的决策,在之前分析启动流程中,关于Task处理的部分,我这里是简化掉了很多的,今天再来分析一下。

入口与计算启动参数

在之前分析Activity的启动中,已经看到了关于处理Task的代码是在ActivityStart当中的startActivityInner方法当中,这个方法有不少入参,先捋一遍: resultTo为调用的Activity的mToken(IBinder)

ActivityRecord r, //新创建的Record,包含calling信息和要打开的ActivityInfo等
ActivityRecord sourceRecord, //resultTo不为空的时候才会去使用`ActivityRecord isInAnyTask`读取
IvoiceInteractionSession voiceSession, //startVoiceActivity的时候才会传
IvoiceInteractor voiceInteractor, //同上,一般为系统的语音助手界面
int startFlags, //客户端传过来的startFlags一般为0
boolean doResume, //是否需要去resume activity,对于启动Activity场景总是为true
ActivityOptions options,  //Activity启动的一些参数,页面跳转动画等
Task inTask, //一般为通过AppTaskImpl启动Activity才会设置值,正常app启动不存在
TaskFragment inTaskFragment, //同上,一般情况为空
int balCode, //Activity后台启动的许可Code,默认为BAL_ALLOW_DEFAULT
NeededGrants intentGrants //Intent访问权限授权

有了所有的入参可以看看computeLaunchingTaskFlags,对于普通应用mInTask为空,mSourceRecord不为空,关注这个方法内的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (mInTask == null) {  
    if (mSourceRecord == null) {  
        if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {  
	        //如果获取不到启动来源的ActivityRecord,且当前要启动的Activity还没有设置NEW_TASK flag,则给他添加
            mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
        }  
    } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) { 
	    //如果来源ActivityRecord是SINGLE INSTANCE,也就是说它是自己独立的任务栈,新启动Activity必须设置NEW_TASK 
        mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
    } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {  
        //如果新启动的Activity是SingleInstance或者SingleTask,也要添加NEW_TASK flag
        mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
    }  
}  
  
if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0  
        && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {  
    //如果要启动的Activity设置了分屏的FLAG,但是却没有设置NEW——FLAG或者没有源ActivityRecord,这个时候就需要忽略掉分屏的这个FLAG
    mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;  
}

简化版本的流程图如下:

获取当前的顶部Task: getFocusedRootTask

以上是针对LaunchFlag的一部分处理,但并不是全部,暂时继续往后看。随后就是获取task

1
2
3
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();  
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;  
final Task reusedTask = getReusableTask();

首先看这个mPreferredTaskDisplayArea,这个表示倾向的Activity显示的TaskDisplay,它的赋值是在前面的setInitialState方法中:

1
2
3
4
5
mSupervisor.getLaunchParamsController().calculate(inTask, r.info.windowLayout, r,  
        sourceRecord, options, mRequest, PHASE_DISPLAY, mLaunchParams);  
mPreferredTaskDisplayArea = mLaunchParams.hasPreferredTaskDisplayArea()  
        ? mLaunchParams.mPreferredTaskDisplayArea  
        : mRootWindowContainer.getDefaultTaskDisplayArea();

我们这里就以它是拿的DefaultTaskDisplayArea为例来分析,继续就是看它的getFocusedRootTask,看代码之前先看看这些类的关系图,之前画过Task,WindowContainer相关的,但是还不够全,这里再补充完整一点。

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer {

List<WindowContainer> mChildren

}
class TaskFragment
class Task 
class ActivityRecord {
Task task
TaskDisplayArea mHandoverTaskDisplayArea
}
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
WindowContainer <|-- DisplayArea
DisplayArea <|-- TaskDisplayArea
WindowContainer"0..*" <-- "1*"WindowContainer
WindowContainer <|-- WindowToken
WindowToken <|--ActivityRecord
ActivityRecord --> Task
ActivityRecord --> TaskDisplayArea

当然WindowContainer的子类远不止这些,包括WindowState等等都是它的子类,但是暂时不需要讨论他们,这里暂时先不列出来了。 我们还是先看getFocusedRootTask方法的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getFocusedRootTask() {  
    if (mPreferredTopFocusableRootTask != null) {  
        return mPreferredTopFocusableRootTask;  
    }  
  
    for (int i = mChildren.size() - 1; i >= 0; --i) {  
        final WindowContainer child = mChildren.get(i);  
        if (child.asTaskDisplayArea() != null) {  
            final Task rootTask = child.asTaskDisplayArea().getFocusedRootTask();  
            if (rootTask != null) {  
                return rootTask;  
            }  
            continue;  
        }  
  
        final Task rootTask = mChildren.get(i).asTask();  
        if (rootTask.isFocusableAndVisible()) {  
            return rootTask;  
        }  
    }  
  
    return null;  
}

如果说当前的TaskDisplayArea中,preferredTopFocusableRoot存在就会直接使用,这个会在postionChildTaskAt的时候,如果child放置到顶部,并且它是可获得焦点的,会把他赋值给这个preferredTopFocusableRoot。 我们这里先看它为空的情况。如果它为空,这回到树状结构中查找,遍历树节点如果也是TaskDisplayArea,则会 看他们的focusedRootTask是否存在,如果就返回。如果节点是Task,就会检查这个Task是否为可获得焦点并且可见的,则返回它。否则就返回空。因为我们当前已经打了Activity,这里一般是可以获得值的。

如果拿到了prevTopRootTask,就会去调用getTopLeafTask去获取叶子节点的Task,代码如下:

1
2
3
4
5
6
7
8
public Task getTopLeafTask() {  
    for (int i = mChildren.size() - 1; i >= 0; --i) {  //从大数开始遍历
        final Task child = mChildren.get(i).asTask();  
        if (child == null) continue;  //如果不是Task就跳过
        return child.getTopLeafTask();  //继续看它的子节点
    }  
    return this;  //没有孩子节点,那就是一个叶子节点 
}

以上是获取叶子节点的代码,典型的树的遍历代码。到目前是拿的当前在展示的页面的任务栈。

获取可复用的Task:getReusableTask

而之后的getReusableTask则是获取可以使用的任务Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private Task getReusableTask() {  
    //一般是从最近任务打开的页面才会执行这里,我们可以跳过
    if (mOptions != null && mOptions.getLaunchTaskId() != INVALID_TASK_ID) {  
        Task launchTask = mRootWindowContainer.anyTaskForId(mOptions.getLaunchTaskId());  
        if (launchTask != null) {  
            return launchTask;  
        }  
        return null;  
    }  
	//如果启动的FLAG是 Single Instance或者SingleTask;又或者是虽然设置了NEW_TASK但是没有设置MULTIPLE_TASK。这些情况都会把新的Activity放到已有的任务栈。
    boolean putIntoExistingTask = ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0 &&  
            (mLaunchFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0)  
            || isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK);  
    //因为mInTask为空,后面的resultTo不为空,因此putIntoExistingTask结果为false。当通过startActivityForResult的且requestCode > 0 时候就不为空
    putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null;  
    ActivityRecord intentActivity = null;  
    if (putIntoExistingTask) {  
        if (LAUNCH_SINGLE_INSTANCE == mLaunchMode) {  
            //这种情况只有一个实例,就通过intent和activityInfo去找到它。
            intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,  
                   mStartActivity.isActivityTypeHome());  
        } else if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {  
            //对于分屏的,如果历史栈中有才使用
            intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,  
                    !(LAUNCH_SINGLE_TASK == mLaunchMode));  
        } else {  
            // 查找最合适的Task给Activity用  
            intentActivity =  
                    mRootWindowContainer.findTask(mStartActivity, mPreferredTaskDisplayArea);  
        }  
    }  
  
    if (intentActivity != null && mLaunchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK  
            && !intentActivity.getTask().getRootActivity().mActivityComponent.equals(  
            mStartActivity.mActivityComponent)) {  
            //对于singleInstancePreTask,如果Task的根Activity不是要启动的Activity那么还是不能够复用,因此需要把intentActivity设置为空。
        intentActivity = null;  
    }  
  
    if (intentActivity != null  
            && (mStartActivity.isActivityTypeHome() || intentActivity.isActivityTypeHome())  
            && intentActivity.getDisplayArea() != mPreferredTaskDisplayArea) {  
        //
        intentActivity = null;  
    }  
  
    return intentActivity != null ? intentActivity.getTask() : null;  
}

以上就是根据条件判断是否可以复用栈,如果可以会去拿已经存在的Activity,如果Activity存在,则回去拿它的Task。其中这里有一个singleInstancePreTask的启动模式,这个对于我们很多Android开发这是不熟悉的,它是Android12引入的,它可以说是加强版本的singleInstance,当它是Task栈的根Task的时候就复用,如果不是的就类似singleTask会去打开一个新的Task栈。

这里先来看一下这个findActivity,他也是到RootWindowContainer中去查找,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ActivityRecord findActivity(Intent intent, ActivityInfo info, boolean compareIntentFilters) {  
    ComponentName cls = intent.getComponent();  
    if (info.targetActivity != null) {  
        cls = new ComponentName(info.packageName, info.targetActivity);  
    }  
    final int userId = UserHandle.getUserId(info.applicationInfo.uid);  
  
    final PooledPredicate p = PooledLambda.obtainPredicate(  
            RootWindowContainer::matchesActivity, PooledLambda.__(ActivityRecord.class),  
            userId, compareIntentFilters, intent, cls);  
    final ActivityRecord r = getActivity(p);  
    p.recycle();  
    return r;  
}

其中第8行就是创建了一个PooledPredicate,在我们调用test方法的时候就 会调用RootWindowContainer::matchesActivity这个方法,这个方法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static boolean matchesActivity(ActivityRecord r, int userId,  
        boolean compareIntentFilters, Intent intent, ComponentName cls) {  
    if (!r.canBeTopRunning() || r.mUserId != userId) return false;  
  
    if (compareIntentFilters) {  
        if (r.intent.filterEquals(intent)) {  
            return true;  
        }  
    } else {  
        if (r.mActivityComponent.equals(cls)) {  
            return true;  
        }  
    }  
    return false;  
}

首先检查,对应的ActivityRecord是否可以运行在topTask,是否与我们目标要启动的Activity是同样的用户Id,也就是在同一个进程。如果compareIntentFilters为true,还是检查他们的intent是否相同,之后会检查是否为同一个Activity类。对于这个有所了解,我们继续看getActivity的代码,它首先是会调用WindowContainer中的这个方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,  
        ActivityRecord boundary) {  
    if (traverseTopToBottom) {  
        for (int i = mChildren.size() - 1; i >= 0; --i) {  
            final WindowContainer wc = mChildren.get(i);  
            if (wc == boundary) return boundary;  
  
            final ActivityRecord r = wc.getActivity(callback, traverseTopToBottom, boundary);  
            if (r != null) {  
                return r;  
            }  
        }  
    } else {  
        ...
    }  
  
    return null;  
}

如果单看上面的代码,我们似乎永远都拿不到ActivityRecord,但是呢ActivityRecord也是WindowContainer的子类,在它当中我们也有同名方法,代码如下:

1
2
3
4
5
@Override  
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,  
        ActivityRecord boundary) {  
    return callback.test(this) ? this : null;  
}

这里可以看到,他就是调用了我们刚刚传入的那个PooledPredicate来测试自己是否符合要求,从而我们可以拿到对应的ActivityRecord

计算目标Task: computeTargetTask

到这里我们可以继续看startActivityInner方法中的如下代码:

1
2
3
final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();  
final boolean newTask = targetTask == null;  
mTargetTask = targetTask;

如果我们刚刚已经拿到reusedTask,那么目标的task就会使用它,如果拿不到则会调用computeTargetTask去获取Task,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private Task computeTargetTask() {  
    if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask  
            && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {  
        // 同时满足这些条件的情况,不复用task,直接返回空
        return null;  
    } else if (mSourceRecord != null) {  
	    //调用源ActivityRecord,直接复用调用源的Task
        return mSourceRecord.getTask();  
    } else if (mInTask != null) {  
        //inTask一般是AppTaskImpl指定的,就直接用它,它有可能还没创建这里去创建
        if (!mInTask.isAttached()) {  
            getOrCreateRootTask(mStartActivity, mLaunchFlags, mInTask, mOptions);  
        }  
        return mInTask;  
    } else {  
	    //获取或者创建Task
        final Task rootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, null /* task */,  
                mOptions);  
        final ActivityRecord top = rootTask.getTopNonFinishingActivity();  
        if (top != null) {  
            return top.getTask();  
        } else {  
            rootTask.removeIfPossible("computeTargetTask");  
        }  
    }  
    return null;  
}

这里我们继续去看一下getOrCreateRootTask,代码如下:

1
2
3
4
5
6
7
8
private Task getOrCreateRootTask(ActivityRecord r, int launchFlags, Task task,  
        ActivityOptions aOptions) {  
    final boolean onTop =  
            (aOptions == null || !aOptions.getAvoidMoveToFront()) && !mLaunchTaskBehind;  
    final Task sourceTask = mSourceRecord != null ? mSourceRecord.getTask() : null;  
    return mRootWindowContainer.getOrCreateRootTask(r, aOptions, task, sourceTask, onTop,  
            mLaunchParams, launchFlags);  
}

这里还是先拿到调用端的sourceTask以及是否需要onTop,之后调用了RootWindowContainergetOrCreateRootTask方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Task getOrCreateRootTask(@Nullable ActivityRecord r,  
        @Nullable ActivityOptions options, @Nullable Task candidateTask,  
        @Nullable Task sourceTask, boolean onTop,  
        @Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {  
    ...
    TaskDisplayArea taskDisplayArea = null;  
    
    final int activityType = resolveActivityType(r, options, candidateTask);  
    Task rootTask = null;  
    ...
    int windowingMode = launchParams != null ? launchParams.mWindowingMode  
            : WindowConfiguration.WINDOWING_MODE_UNDEFINED;  
    ....
    if (taskDisplayArea == null) {  
        taskDisplayArea = getDefaultTaskDisplayArea();  
    }  
    return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, sourceTask,  
            launchParams, launchFlags, activityType, onTop);  
}

因为我们没有设置什么参数,因此会执行到最后的fallback流程,我们只分析这一部分。默认我们拿到的activityTypeActivity_TYPE_STANDARDgetDefaultTaskDisplayArea会拿到默认的TaskDisplayArea这个之前已经分析过了,最后就是通过它去调用getOrCreateRootTask,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,  
        @Nullable Task candidateTask, @Nullable Task sourceTask,  
        @Nullable ActivityOptions options, int launchFlags) {  
    final int resolvedWindowingMode =  
            windowingMode == WINDOWING_MODE_UNDEFINED ? getWindowingMode() : windowingMode;  
    if (!alwaysCreateRootTask(resolvedWindowingMode, activityType)) {  
        Task rootTask = getRootTask(resolvedWindowingMode, activityType);  
        if (rootTask != null) {  
            return rootTask;  
        }  
    } else if (candidateTask != null) {  
        ....
    }  
    return new Task.Builder(mAtmService)  
            .setWindowingMode(windowingMode)  
            .setActivityType(activityType)  
            .setOnTop(onTop)  
            .setParent(this)  
            .setSourceTask(sourceTask)  
            .setActivityOptions(options)  
            .setLaunchFlags(launchFlags)  
            .build();  
}

因为我们传进来的windowingModeWINDOWING_MODE_UNDEFINED,因此这里会调用getWindowingMode来设置Mode,这里就是调用系统设置了,不需要看代码。

因为ActivityType是ACTIVITY_TYPE_STAND,所以这里alwaysCreateRootTask为true,因为我们传进来的candidateTask也是空,因此最后就是会创建一个新的Task。但是因为是创建的新task,这个Task里面没有运行中的Activity,因此computeTargetTask还是会返回空。

获取PriorAboveTask和task回收检查

继续回来看startActivityInner内部的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (targetTask != null) {   //在DisplayArea中获取在targetTask Root上面的其他root task
    mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask());  
}  
//如果newTask为false,则看看目标task 顶部的未finish的ActivityRecord
final ActivityRecord targetTaskTop = newTask  
        ? null : targetTask.getTopNonFinishingActivity();  
if (targetTaskTop != null) {  
    startResult = recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);  
    if (startResult != START_SUCCESS) {  
        return startResult;  
    }  
} else {  
    mAddingToTask = true;  
}

在可以复用栈的情况下,targetTaskTop不为空,比如singleTask的模式,这个时候会去执行recycleTask。其他情况设置mAddingToTask,表示我们的ActivityRecord需要添加到Task。

1
2
3
4
5
6
7
final Task topRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();  
if (topRootTask != null) {  
    startResult = deliverToCurrentTopIfNeeded(topRootTask, intentGrants);  
    if (startResult != START_SUCCESS) {  
        return startResult;  
    }  
}

如果我们检查topRootTask不为空的情况,这里如果我们的启动模式是singleTask,首先会检查task栈顶未启动的Activity是否与当前要启动的相同,如果相同,则不启动当前Activity,仅仅去执行它的newIntent,具体代码就不分析了。

创建RootTask,处理新Activity的Task

再往后看代码,之后就该创建RootTask了,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (mTargetRootTask == null) {  
    mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,  
            mOptions);  
}  
if (newTask) {  
    final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)  
            ? mSourceRecord.getTask() : null;  
    setNewTask(taskToAffiliate);  
} else if (mAddingToTask) {  
    addOrReparentStartingActivity(targetTask, "adding to task");  
}

上面调用了getOrCreateRootTask,来创建了新的RootTask,与我们之前分析的类似。同时因为我们之前没有成功创建targetTask,因此这里会执行到setNewTask,而taskToAffiliate没有特殊参数,默认我们先按照空来对待吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private void setNewTask(Task taskToAffiliate) {  
    final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;  
    final Task task = mTargetRootTask.reuseOrCreateTask(  
            mStartActivity.info, mIntent, mVoiceSession,  
            mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);  
    task.mTransitionController.collectExistenceChange(task);  
    //把新的ActivityRecord放置到Task列表的顶部
    addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");  
    if (taskToAffiliate != null) {  
        mStartActivity.setTaskToAffiliateWith(taskToAffiliate);  
    }  
}

这里大多数情况,toTop会是true,我们去看一下这个reuseOrCreateTask方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Task reuseOrCreateTask(ActivityInfo info, Intent intent, IVoiceInteractionSession voiceSession,  
        IVoiceInteractor voiceInteractor, boolean toTop, ActivityRecord activity,  
        ActivityRecord source, ActivityOptions options) {  
  
    Task task;  
    if (canReuseAsLeafTask()) {  //如果没有Task子节点或者不是组织创建的
        task = reuseAsLeafTask(voiceSession, voiceInteractor, intent, info, activity);  
    } else {  
        // 创建taskId
        final int taskId = activity != null  
                ? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId)  
                : mTaskSupervisor.getNextTaskIdForUser();  
        final int activityType = getActivityType();  
        //创建task,并且当前Task设置为这个Task的Parent,在build当中,把当前的Task放置到Parent的mChildren当中,根据toTop决定是否放置到顶部
        task = new Task.Builder(mAtmService)  
                .setTaskId(taskId)  
                .setActivityType(activityType != ACTIVITY_TYPE_UNDEFINED ? activityType  
                        : ACTIVITY_TYPE_STANDARD)  
                .setActivityInfo(info)  
                .setActivityOptions(options)  
                .setIntent(intent)  
                .setVoiceSession(voiceSession)  
                .setVoiceInteractor(voiceInteractor)  
                .setOnTop(toTop)  
                .setParent(this)  
                .build();  
    }  
  
    int displayId = getDisplayId();  
    if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;  
    final boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController()  
            .isKeyguardOrAodShowing(displayId);  
    if (!mTaskSupervisor.getLaunchParamsController()  
            .layoutTask(task, info.windowLayout, activity, source, options)  
            && !getRequestedOverrideBounds().isEmpty()  
            && task.isResizeable() && !isLockscreenShown) {  
            //设置task的布局边界
        task.setBounds(getRequestedOverrideBounds());  
    }  
  
    return task;  
}

上面代码我们就可以复用或者创建新的task,详见注释。拿到Task,或者我们之前已经有Task的情况下(mAddingToTask为true)的时候,还需要执行addOrReparentStartingActivity,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {  
    TaskFragment newParent = task;  
    if (mInTaskFragment != null) {  
        //我们的场景不涉及InTaskFragment不为空,忽略
        ...
    } else {  
	    //当clearTop的时候,并且是可嵌入的,这个时候会保存TaskFragment到mAddingToTaskFragment
        TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;  
        if (candidateTf == null) {  
	        //获取目标Task的topRunningActivity,新建的Task不存在
            final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,  
                    false /* includingEmbeddedTask */);  
            if (top != null) {  
                candidateTf = top.getTaskFragment();  
            }  
        }  
        if (candidateTf != null && candidateTf.isEmbedded()  
                && canEmbedActivity(candidateTf, mStartActivity, task) == EMBEDDING_ALLOWED) {  
                //如果拿到了topTask,并且对应的Task是可嵌入的,并且要打开的ActivityRecord也可被嵌入,这把拿到的这个Task作为新的父Task
            newParent = candidateTf;  
        }  
    }  
    //新的ActivityRecord的TaskFragment为空,或者和新的Parent一样,就把这个ActivityRecord放到Task的顶部
    if (mStartActivity.getTaskFragment() == null  
            || mStartActivity.getTaskFragment() == newParent) {  
        newParent.addChild(mStartActivity, POSITION_TOP);  
    } else {  
        mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);  
    }  
}

这里会检查如果新的父Task和我们可以复用的Task是否相同,如果相同,或者ActivityRecord中还没有parent,这个时候就把ActivityRecord添加到Task的孩子列表的顶部。而如果ActivityRecord已经存在了parent并且不是我们将要设置的这个,就需要做reparent,这个步骤代码比较复杂,前面调用检查判断的调用省略,直接看最后的调用,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowContainer.java
void reparent(WindowContainer newParent, int position) {  
    final DisplayContent prevDc = oldParent.getDisplayContent();  
    final DisplayContent dc = newParent.getDisplayContent();  
  
    mReparenting = true;  
    //从旧的parent中移除自己,并把自己添加到新parent的指定位置
    oldParent.removeChild(this);  
    newParent.addChild(this, position);  
    mReparenting = false;  
  
    // 重新布局layout
    dc.setLayoutNeeded();  
    //如果新旧的DisplayContent不同,还需要做displayChange的处理
    if (prevDc != dc) {  
        onDisplayChanged(dc);  
        prevDc.setLayoutNeeded();  
    }  
    getDisplayContent().layoutAndAssignWindowLayersIfNeeded();  
  
    onParentChanged(newParent, oldParent);  
    onSyncReparent(oldParent, newParent);  
}

以上的代码我们看到有做parent的替换,但是复杂点在后面的onParentChanged里面,这里会做SurfaceControl的创建或者reparent,这里就不深入了。除此之外,这里还涉及到动画的处理,我们这里也 不深入了。

继续往后看

1
2
3
4
if (!mAvoidMoveToFront && mDoResume) {  
    mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);  
    ... 
}

这里会在检查我们的TargetRootTask相比与它的RootTask如果不是在顶部的,需要把它移动到顶部。再往后面就是调用TargetRootTask去启动Activity,以及确认Activity显示出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();  
//启动Activity
mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,  
        mOptions, sourceRecord);  
if (mDoResume) {  
    final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();  
    if (!mTargetRootTask.isTopActivityFocusable()  
            || (topTaskActivity != null && topTaskActivity.isTaskOverlay()  
            && mStartActivity != topTaskActivity)) { 
            //如果当前要启动的Activity还没有启动,没有在栈顶端,执行下面的代码
        mTargetRootTask.ensureActivitiesVisible(null /* starting */,  
                0 /* configChanges */, !PRESERVE_WINDOWS);  
        mTargetRootTask.mDisplayContent.executeAppTransition();  
    } else {  
        if (mTargetRootTask.isTopActivityFocusable()  
                && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) {  
            mTargetRootTask.moveToFront("startActivityInner");  
        }  
        mRootWindowContainer.resumeFocusedTasksTopActivities(  
                mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);  
    }  
}  
mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask);   //更新用户的rootTask
  
// 更新系统的最近任务
mSupervisor.mRecentTasks.add(startedTask);

到此位置才算是完成了所有Task计算以及Activity的启动。

通过Adb shell看Activity Task栈

前面都是在解读Android的源码可能比较抽象,其中涉及到了挺多WindowContainer和Task等等相关的查找创建的,为了更加形象。我写了个小demo,主页面是普通的launchMode,另外一次打开了一个singleTask启动Mode的和一个singleInstance 启动Mode的页面,然后我们用一下命令进行Activity Task的dump:

1
adb shell dumpsys activity activities > ~/activitytasks.txt

我们就得到了如下的内容(为方便解读,做了删减):

Display #0 (activities from top to bottom):
  * Task{8ed7532 #40 type=standard A=10116:com.example.myapplication U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
    topResumedActivity=ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}
    * Hist  #0: ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}

  * Task{ac77886 #39 type=standard A=10116:com.example.myapplication U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
    mLastPausedActivity: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
    * Hist  #1: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
    * Hist  #0: ActivityRecord{ef92174 u0 com.example.myapplication/.MainActivity} t39}

  * Task{d8527c1 #1 type=home U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
    * Task{d60ff49 #33 type=home I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 rootTaskId=1 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
      mLastPausedActivity: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}
      * Hist  #0: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}


  * Task{2c52978 #36 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
    mLastPausedActivity: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}
    mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
    isSleeping=false
    * Hist  #0: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}


  * Task{e38c1d6 #35 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
    mLastPausedActivity: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}
    mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
    isSleeping=false
    * Hist  #0: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}

  * Task{1d65c74 #3 type=undefined U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
    mCreatedByOrganizer=true
    * Task{41cb5e3 #5 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
      mBounds=Rect(0, 2960 - 1440, 4440)
      mCreatedByOrganizer=true
      isSleeping=false
    * Task{1cdca12 #4 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
      mCreatedByOrganizer=true
      isSleeping=false

这上面就是我们的mRootContainer它当中的的display下面的所有的Task记录,因为我的手机只有一块屏幕,这里只有一个display0, 并且展示了他们的存储关系,这里我们可以看到我们的SimpleInstanceActivity它是在独立的Task当中的。用图表简单描绘一下,结构如下所示:

总结

以上就是Activity Task管理的分析,因为这个流程真的是非常复杂,因此中间的很多步骤还是进行了部分省略。Android系统迭代了这么多年,作为UI展示的组件,Activity承载了太多东西,多屏幕,折叠屏什么的都要支持,因此引入的东西就越来越多。官方也是意识到了这一块的,Activity的管理从AMS抽出来单独的ATMS,ActivityTaskSupervisor的功能也在慢慢抽离到其他的代码中,当前代码里面也添加了很多注释,只要花时间还是能够给搞明白的。

本文仅为一家之言,因为个人疏忽,可能文中也会出现一些错误,欢迎大家指正。

看完评论一下吧

Android源码分析:Activity启动流程分析

2024-09-11 22:20:43

Activity是Android中四大组件使用最多的一种,不准确的说,一个Activity就是一个独立页面的承载,因此看Android系统的源码,Activity的启动也是必须要去阅读的。今天的文章就来介绍Activity的启动。因为之前的文章已经分析了ClientTransaction,因此我们对于AMS调用Activity的生命周期和启动有所了解。并且我们也已经分析过了Binder,对于跨进程通讯我们也比较清楚了,不需要细看。我们也分析了应用进程的启动,我们分析Activity启动过程,就不需要去关注应用进程的启动了。有了这些基础,分析Activity的启动会容易一点点。

发起启动Activity

我们首先来看一下启动Activity的调用,我们通常会使用下面的显示调用来启动一个Activity。

1
2
Intent intent = new Intent(context, AActivity.class);
startActivity(intent);

当然也有可能会使用隐式调用来启动一个Activity,如下:

1
2
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://isming.me"));
startActivity(intent);

不过以上两者在启动过程中,仅仅是查找目标组件有区别,并且对于隐式调用,可能存在多个可以启动的Activity,这个时候需要让用户选择目标的页面。对于这一块,我们在后面这个地方会考虑部分略过。

Activity中最终会走到 startActivityForResult(intent, requestCode, options)方法中,这里传入的options我们可以用它设置一些东西,比如App跳转的动画等,我们前面的场景的options为空,并且手机上默认的parent activity也为空,因此会执行这一部分:

1
2
3
4
5
6
7
8
Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
if (ar != null) {
    mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
}

发起端的处理

在Instrumentation.execStartActivity中会调用如下代码:

1
2
3
4
5
6
int result = ActivityTaskManager.getService().startActivity(whoThread,
                    who.getOpPackageName(), who.getAttributionTag(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()), token,
                    target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
            notifyStartActivityResult(result, options);
            checkStartActivityResult(result, intent);

早期是直接通过ActivityManagerService去启动新的页面的,在这个commit开始把Activity管理的拆分到ActivityTaskManagerService中去。这里我们看到是去获取ActivityTaskManagerService后面简称ATMS,获取ATMS的代码就不罗列了。

这里传到ATMS的参数,包括,发起应用的ApplicationThread,包名(对于普通应用来说opPackage和packageName是一样的),启动的Intent,token和target一般都是空。

ATMS执行startActivity

最终的执行实际是通过binder调用到ActivityTaskManagerService中的startActivity方法,这个方法中又直接调用了startActivityAsUser,其中会有一些检查,检查调用端的uid和packageName是否匹配和其他一些检查,这里不太关注,我们主要关注以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
userId = getActivityStartController().checkTargetUser(userId, validateIncomingUser,
                Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");
        return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
                .setCaller(caller)
                .setCallingPackage(callingPackage)
                .setCallingFeatureId(callingFeatureId)
                .setResolvedType(resolvedType)
                .setResultTo(resultTo)
                .setResultWho(resultWho)
                .setRequestCode(requestCode)
                .setStartFlags(startFlags)
                .setProfilerInfo(profilerInfo)
                .setActivityOptions(opts)
                .setUserId(userId)
                .execute();

从上面的逻辑可以看到,控制Activity启动的代码都放到ActivityStartController中了,首先是获取用户uid,因为每个应用的都会有一个uid,其后就是获取一个ActivityStarter,再通过构建者模式把启动Activity的参数都传到ActivityStarter中去,最后在ActivityStarter的execute()方法中去执行启动的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
if (mRequest.activityInfo == null) { //如果还没有activitgyInfo去填充
                mRequest.resolveActivity(mSupervisor);
}
...
synchronized (mService.mGlobalLock) {
	res = resolveToHeavyWeightSwitcherIfNeeded(); //检查是否为heavy-weight 进程,系统会限制同一时间只有一个heavy-weight进程
    if (res != START_SUCCESS) {
        return res;
    }
    res = executeRequest(mRequest);
}

其中第2行代码就是根据我们的Intent去查询我们将要打开的目标Activity信息。

解析ActivityInfo

resolveActivity中的核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//ActivityStarter.Request
void resolveActivity(ActivityTaskSupervisor supervisor) {
	resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,  
        0 /* matchFlags */,  
        computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid));
    activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags,  
        profilerInfo);
    if (activityInfo != null) {  
	    intentGrants = supervisor.mService.mUgmInternal.checkGrantUriPermissionFromIntent(  
            intent, resolvedCallingUid, activityInfo.applicationInfo.packageName,  
            UserHandle.getUserId(activityInfo.applicationInfo.uid));  
	}
}

resolveActivity的工作主要由ActivityTaskSupervisor来完成,首先是resolveIntent来获取ResolveInfo,之后调用resolveActivity获取ActivityInfo,最后再去对Intent中的data Uri做权限检查,我们这里只需要分析前两步骤就可。

resolveIntent方法内部,我们看到是调用了PackageManagerServiceresolveIntent方法,代码如下,具体就不深入探究了。

1
2
3
4
//ActivityTaskSupervisor
return mService.getPackageManagerInternalLocked().resolveIntent(  
        intent, resolvedType, modifiedFlags, privateResolveFlags, userId, true,  
        filterCallingUid);

resolveActivity代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags,  
        ProfilerInfo profilerInfo) {  
    final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;  
    if (aInfo != null) {  
        intent.setComponent(new ComponentName(  
                aInfo.applicationInfo.packageName, aInfo.name));  
        ... 
    }  
    return aInfo;  
}

这里所做的事情则比较简单,就是从前面拿到的ResolveInfo中拿到activityInfo,并且构建一个ComponentName放到Intent中去。到此为止就拿到了要打开的Activity信息。

ActivityStarter.executeRequest

在前面拿到ActivityInfo,并且我们还构建了一个Request,我们就会继续调用executeRequest方法,其中是有大段的代码 是检查权限,以及一些系统Activity逻辑的处理,不是我们流程关注的重点,重要的是以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage,  
        callingFeatureId);  
if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment,  
        callingPid, callingUid, checkedOptions)) {  //拦截器对要启动的Activity做预处理
    intent = mInterceptor.mIntent;  
    rInfo = mInterceptor.mRInfo;  
    aInfo = mInterceptor.mAInfo;  
    resolvedType = mInterceptor.mResolvedType;  
    inTask = mInterceptor.mInTask;  
    callingPid = mInterceptor.mCallingPid;  
    callingUid = mInterceptor.mCallingUid;  
    checkedOptions = mInterceptor.mActivityOptions;  
  
    intentGrants = null;  
}

final ActivityRecord r = new ActivityRecord.Builder(mService) //构建ActivityRecord
                .setCaller(callerApp)
                .setLaunchedFromPid(callingPid)
                .setLaunchedFromUid(callingUid)
                .setLaunchedFromPackage(callingPackage)
                .setLaunchedFromFeature(callingFeatureId)
                .setIntent(intent)
                .setResolvedType(resolvedType)
                .setActivityInfo(aInfo)
                .setConfiguration(mService.getGlobalConfiguration())
                ...
                .build();

        mLastStartActivityRecord = r; //保存构建的ActivityRecord

       ...

        mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
                request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
                inTask, inTaskFragment, balCode, intentGrants); //执行启动Activity,并保存结果到mLastStartActivityResult中,以及结果中返回这个result

        if (request.outActivity != null) {
            request.outActivity[0] = mLastStartActivityRecord;
        }
        ...

这里我们有一些权限检查和系统处理之类的没有贴,不过还是贴了一下intercept方法,这里就是给了系统的其他代码来修改Intent的机会。之后就会利用我们传进来的信息去创建ActivityRecord,并且调用startActivityUnchecked去进入下一步:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,...,NeededUriGrants intentGrants) {
        int result = START_CANCELED;
        final Task startedActivityRootTask;

    ......
        try {
            ......
            try {
                .....
                result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
                        startFlags, doResume, options, inTask, inTaskFragment, balCode,
                        intentGrants);
            } finally {
                startedActivityRootTask = handleStartResult(r, options, result, newTransition,  
        remoteTransition); //处理启动Activity的结果
            }
        } finally {
            mService.continueWindowLayout(); //wms处理
        }
        postStartActivityProcessing(r, result, startedActivityRootTask);

        return result;
    }

这里又走到了startActivityInner(),startActivityInner()会去计算launch falgs,去判断是否开创建新的Task还是可以复用task,以及调用启动的后续代码,这个方法的代码比较长我们先一点一点的看。

Task的处理

首先来看其中关于flag的处理,首先就是其中调用的computeLaunchingTaskFlags方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private void computeLaunchingTaskFlags() {  
	...
    if (mInTask == null) {  
        if (mSourceRecord == null) {  
            if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {  
                mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
            }  
        } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {  
            mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
        } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {  
            mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;  
        }  
    }  
  
    if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0  
            && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {  
       mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;  
    }  
}

这里就是对于我们的启动的LaunchFlag做处理,比如说LAUNCH_SIGLE_INSTANCELAUNCH_SINGLE_TASK都给添加FLAG_ACTIVITY_NEW_TASK等。

随后则是计算Task:

1
2
3
4
5
6
7
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();  
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;  
final Task reusedTask = getReusableTask();

final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();  
final boolean newTask = targetTask == null;  
mTargetTask = targetTask;

getFocusedRootTask会尝试去获取首选的Task,如果不存在也会从当前显示屏获取获取最顶部的可触摸并且在展示的Task。而这个preTopTask如果能够获取到,它又会去获取的它叶子节点。叶子节点的规则就是没有只节点。Task相关类的继承结果如下:

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer
class TaskFragment
class Task 
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
1
2
3
4
if (mTargetRootTask == null) {  
    mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,  
            mOptions);  
}

这里最终会拿到RootTask,如果没有也会创建,具体代码这里不分析了。

调用Task的 resumeFocusedTasksTopActivities

之后会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//ActivityStarter
		...
		mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,  
        mOptions, sourceRecord); //这个名字是startActivityLock但并不是真的打开activity,而是把Activity对应的task放到列表的最前面,以及会展示window动画
        if (mDoResume) {
	        if (!mTargetRootTask.isTopActivityFocusable()  
	        || (topTaskActivity != null && topTaskActivity.isTaskOverlay()  
	        && mStartActivity != topTaskActivity)) { 
	         //对样式pip页面或者其他一些情况的处理
	         ...
			} else {
			...  
		    //真正的启动Activity的代码这里是入口
		    mRootWindowContainer.resumeFocusedTasksTopActivities(  
	            mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);  
			}
			...
			
        }
        ...

        return START_SUCCESS;
    }

我们看到首先调用了startActivityLocked方法,这里主要做的就是把我们的ActivityReccord放到Task中去,并且展示Activity的启动动画。之后调用的RootContainerresumeFocsedTasksTopActivities才是真正的启动,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//RootWindowContainer.java
boolean resumeFocusedTasksTopActivities(Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,  
boolean deferPause) {
	...
	boolean result = false;  
	if (targetRootTask != null && (targetRootTask.isTopRootTaskInDisplayArea()  
        || getTopDisplayFocusedRootTask() == targetRootTask)) {  
		result = targetRootTask.resumeTopActivityUncheckedLocked(
			target,targetOptions,  deferPause);  //执行启动
	}
	...
}

后面会走到Task的resumeTopActivityUnCheckedLocked方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options,  
        boolean deferPause) {
	...
	if (isLeafTask()) {
                if (isFocusableAndVisible()) { //可触摸可见
                    someActivityResumed = resumeTopActivityInnerLocked(prev, options, deferPause);
                }
            } else {
                int idx = mChildren.size() - 1;
                while (idx >= 0) {
                    final Task child = (Task) getChildAt(idx--);
                    if (!child.isTopActivityFocusable()) {
                        continue;
                    }
                    if (child.getVisibility(null /* starting */)
                            != TASK_FRAGMENT_VISIBILITY_VISIBLE) {
                        if (child.topRunningActivity() == null) {
                            continue;
                        }
                        break;
                    }

                    someActivityResumed |= child.resumeTopActivityUncheckedLocked(prev, options,
                            deferPause);
                    if (idx >= mChildren.size()) {
                        idx = mChildren.size() - 1;
                    }
                }
            }


}

此处如果当前的Task本来就是叶子节点,那么会调用resumeTopActivityInnerLocked方法,否则会遍历子的task列表,在子task列表中找到符合条件的去执行resumeTopActivityUncheckedLocked方法,如此最后还是会调用到resumeTopActivityInnerLocked方法,而我们再跟进去看,可以看到其中的核心逻辑是调用topFragment的resumeTopActivity方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,  
        boolean deferPause) {
    ...
    boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);  
	if (mResumedActivity != null) {  
	    pausing |= startPausing(mTaskSupervisor.mUserLeaving, false /* uiSleeping */,  
            next, "resumeTopActivity");   //首先把顶部处于Resumed状态的activity执行pausing
}  
if (pausing) {  
    //检查即将启动的Activity的Activity的进程有没有起来,如果没有进程去创建进程,创建进程的代码需要单独分析,此处略过
    if (next.attachedToProcess()) {   
        next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,  
                true /* activityChange */, false /* updateOomAdj */,  
                false /* addPendingTopUid */);  
    } else if (!next.isProcessRunning()) {  
        final boolean isTop = this == taskDisplayArea.getFocusedRootTask();  
        mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,  
                isTop ? HostingRecord.HOSTING_TYPE_NEXT_TOP_ACTIVITY  
                        : HostingRecord.HOSTING_TYPE_NEXT_ACTIVITY);  
    }  
	    ...
	    return true;
	} 
	if (next.attachedToProcess()) { //如何Activity已经在这个进程中了
		...
		final ClientTransaction transaction =  
        ClientTransaction.obtain(next.app.getThread(), next.token); //构建ClientTransaction,传入要打开的Activity对应的applicationThread和IBinder
        ...
        if (next.newIntents != null) {   //把intent放进去,后面会把Activity吊起,并 调用onNewIntent()
		    transaction.addCallback(  
	            NewIntentItem.obtain(next.newIntents, true /* resume */));  
		}
		...
		mAtmService.getLifecycleManager().scheduleTransaction(transaction);
	
	} else {
		...
		mTaskSupervisor.startSpecificActivity(next, true, true); //启动Activity
	}


}

上面最后的代码可以看到,Activity已经存在的时候是走到onNewIntent, 调用的代码被包装成了ClientTransaction,通过ClientlifecycleManager 的scheduleTransaction方法,最终其实是调用了IApplicationThread的scheduleTransaction,最终通过binder调用到了app进程中的同名方法,这里要去看ActivityThread, ApplicationThread为它的内部类,看它的代码,它实际调用了ActivityThread的同名方法。而启动Activity,我们一路跟着startSpecificActivity()方法进去最终会看到也是通过ClientTransaction,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),  
        System.identityHashCode(r), r.info,  
        // TODO: Have this take the merged configuration instead of separate global  
        // and override configs.  
        mergedConfiguration.getGlobalConfiguration(),  
        mergedConfiguration.getOverrideConfiguration(), r.compat,  
        r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,  
        proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),  
        results, newIntents, r.takeOptions(), isTransitionForward,  
        proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,  
        r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
// Set desired final state.  
final ActivityLifecycleItem lifecycleItem;  
if (andResume) {  
    lifecycleItem = ResumeActivityItem.obtain(isTransitionForward,  
            r.shouldSendCompatFakeFocus());  
} else {  
    lifecycleItem = PauseActivityItem.obtain();  
}  
clientTransaction.setLifecycleStateRequest(lifecycleItem);
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

目标进程执行performLaunchActivity

我们之前已经分析过ClientTransaction,我们知道这个LaunchActivityItem的callback,最后client就是我们的ActivityThread,会执行它的handleLaunchActivity方法,其中最核心的就是如下这一句:

1
final Activity a = performLaunchActivity(r, customIntent);

我们继续往里面看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//content of performLaunchActivity() function
...
//为Activity创建Base Context
ContextImpl appContext = createBaseContextForActivity(r);  
Activity activity = null;  
...
//创建Activity实例,就是通过反射来实例化一个Activity实例
java.lang.ClassLoader cl = appContext.getClassLoader();  
activity = mInstrumentation.newActivity(  
            cl, component.getClassName(), r.intent);  
...
//拿到Application的实例,如果缓存中有就用,没有就创建一个新的,这里也不看具体代码了
Application app = r.packageInfo.makeApplicationInner(false, mInstrumentation);
...
//为Activity创建配置,如里面有语言,屏幕设置等等参数,不具体分析了
Configuration config =  
        new Configuration(mConfigurationController.getCompatConfiguration());  
if (r.overrideConfig != null) {  
    config.updateFrom(r.overrideConfig);  
}
...
//把Activity和baseContext绑定,并且把一些参数附加到Activity实例上去
appContext.setOuterContext(activity);  
activity.attach(appContext, this, getInstrumentation(), r.token,  
        r.ident, app, r.intent, r.activityInfo, title, r.parent,  
        r.embeddedID, r.lastNonConfigurationInstances, config,  
        r.referrer, r.voiceInteractor, window, r.activityConfigCallback,  
        r.assistToken, r.shareableActivityToken);
...
mInstrumentation.callActivityOnCreate(activity, r.state); //这一步完成,Activity里面会执行完onCreate()
...
r.setState(ON_CREATE);

再来具体看一看Activity的attach方法中做了什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
....
mWindow.setWindowManager(  
        (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),  
        mToken, mComponent.flattenToString(),  
        (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();  
mCurrentConfig = config;

可以看到在里面绑定了baseContext,以及创建了我们的PhoneWindow,以及把window和windowManager进行绑定,还有其他一些Activity内部会用到的参数的传递。

而mInstrumentation里面最终是会调用Activity的performCreate,其中则会调用activity的onCreate。这里就不贴相关代码了。

这样我们的Activity才走完onCreate,而剩余步骤,我们之前还设置了LifecycleStateRequestResumeActivityItem,因此这是要让我们的Activity最终进入到Resume状态,具体的可以参看ClientTransaction分析。两篇文章配合着一起,就是完整的Activity启动流程了。

总结

从Activity调用startActivity,一直到A T M S调用ActivityStarter的调用时序图如下:

sequenceDiagram
Activity->>Activity: startActivity
Activity->>Instrumentation:execStartActivity
Instrumentation->>ATMS: startActivity
ATMS->>ActivityStarter: execute

从ActivityStarter调用到新的进程处理的时序图如下(省略了到Activity部分的流程):

sequenceDiagram
ActivityStarter->>ActivityStarter: resolveActivity
ActivityStarter->>ActivityStarter: executeRequest
ActivityStarter->>ActivityStarter: startActivityUnchecked
ActivityStarter->>RootWindowContainer: resumeFocusedTasksTopActivities
RootWindowContainer->>Task: resumeTopActivityUncheckedLocked
Task->>Task: resumeTopActivity
Task->>ActivityTaskSupervisor: startSpecificActivity
ActivityTaskSupervisor->>ActivityTaskSupervisor: realStartActivityLocked
ActivityTaskSupervisor->>ActivityThread: handleLaunchActivity
note right of ActivityTaskSupervisor: 通过ClientTransaction
ActivityThread->>ActivityThread: performLaunchActivity

以上就是一个较为精简的Activity启动的流程。其中省略了不少东西,关于startActivityForResult的情况需要获取到打开的Activity的结果的情况这里还没有讨论。

看代码可以发现Activity的启动过程是非常的复杂的,再加上新版本的Android支持多屏幕,折叠屏,分屏,画中画等等非常多的特性,因而Task的复用,新建就很复杂,因此本文这一部分暂时放下,等到以后在写。

如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

Android源码分析: 应用进程启动分析

2024-09-09 20:11:10

Android应用进程的启动,简单来说就是从zygot进程fork出来一个新进程,并对其进行一些初始化。这样做系统的一些代码和资源等等就不需要重复加载,一些环境变量也都不需要重新设置,可以说是很巧妙的设置。下面就来具体分析一下其初始化过程。

启动时机

应用进程的启动,一般是在创建四大组件,比如说启动Activity,Service,使用ContentProvider,有广播需要处理,这些情况需要创建进程。在我们分析的代码当中,除了这几种情况,BackupAngent也会涉及到创建App进程。

启动进程调用的为AMS当中的startProcessLocked方法, 我们注意看的话,AMS当中还有另一个方法startIsolatedProcess也是用来启动进程的,但是这个方法它启动的进程一般是给系统使用的,我们这里不会分析。

AMS调用启动进程

我们就从AMSstartProcessLocked这个方法开始看起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final ProcessRecord startProcessLocked(String processName,  
        ApplicationInfo info, boolean knownToBeDead, int intentFlags,  
        HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,  
        boolean isolated) {  
    return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,  
            hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,  
            false /* isSdkSandbox */, 0 /* sdkSandboxClientAppUid */,  
            null /* sdkSandboxClientAppPackage */,  
            null /* ABI override */, null /* entryPoint */,  
            null /* entryPointArgs */, null /* crashHandler */);  
}

这里我们传入的参数intentFlags为0,zygotePolicyFlagsZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, allowWhileBootingfalse, isolatedfalse。之后startProcessLocked方法内部用调用了ProcessList的同名方法,其中我们关注的核心语句如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ProcessRecord app;
...
app = getProcessRecordLocked(processName, info.uid); //从缓存中获取ProcessRecord
...
if (app == null) {
		app = newProcessRecordLocked(info, processName, isolated, isolatedUid, isSdkSandbox,  
        sdkSandboxUid, sdkSandboxClientAppPackage, hostingRecord);
} else {
	app.addPackage(info.packageName, info.longVersionCode, mService.mProcessStats);
}
...
final boolean success =  
        startProcessLocked(app, hostingRecord, zygotePolicyFlags, abiOverride);

以上代码可以看到,会先去获取是否有现有的processRecord可用,有的话就拿出来使用,没有的话会创建新的,之后会调用startProcessLocked方法。ProcessList中使用mProcessNames来存储ProcessRecord与processName和uid的对应关系,查找的逻辑就是从map中查找不再关注。newProcessRecordLocked方法则是创建新的ProcessRecord,并且会把各种信息保存到这个record当中去。我们这里可以继续看startProcessLocked方法,,最终会调用这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,  
        int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,  
        String abiOverride) {
    //对于从缓存拿到的ProcessRecord,把原来的信息清掉
    if (app.getPid() > 0 && app.getPid() != ActivityManagerService.MY_PID) {  
	    mService.removePidLocked(app.getPid(), app);  
	    app.setBindMountPending(false);  
	    app.setPid(0);  
	    app.setStartSeq(0);  
	}  
	app.unlinkDeathRecipient();  
	app.setDyingPid(0);  
	...
	final IPackageManager pm = AppGlobals.getPackageManager();  
	permGids pm.getPackageGids(app.info.packageName,   =
        MATCH_DIRECT_BOOT_AUTO, app.userId);  
	StorageManagerInternal storageManagerInternal = LocalServices.getService(  
        StorageManagerInternal.class);  
	mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,  
        app.info.packageName);  //检查外部存储访问权限
	externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,  
        app.info.packageName);  
	if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,  
        app.info.packageName, userId)  
        == PackageManager.PERMISSION_GRANTED) {  //检查安装应用的权限
		    Slog.i(TAG, app.info.packageName + " is exempt from freezer");  
	    app.mOptRecord.setFreezeExempt(true);  
	} 
	if (app.processInfo != null && app.processInfo.deniedPermissions != null) {  
    for (int i = app.processInfo.deniedPermissions.size() - 1; i >= 0; i--) {  
        int[] denyGids = mService.mPackageManagerInt.getPermissionGids(  
                app.processInfo.deniedPermissions.valueAt(i), app.userId);  
        if (denyGids != null) {  
            for (int gid : denyGids) {  
                permGids = ArrayUtils.removeInt(permGids, gid);  
            }  
        }  
	    }  
	}  
  
	gids = computeGidsForProcess(mountExternal, uid, permGids, externalStorageAccess); //根据前面的权限和相关信息,计算新启动的进程 需要分配的用户
	...
	//读取app的debuggable,profileable等标志位
	boolean debuggableFlag = (app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;   
	boolean isProfileableByShell = app.info.isProfileableByShell();  
	boolean isProfileable = app.info.isProfileable();
	if (debuggableFlag) {  
	    runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;  
	    runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;  
	    runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;  
  

	    if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),  android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {  
	        runtimeFlags |= Zygote.DISABLE_VERIFIER;  
	    }  
	}
	if (isProfileableByShell) {  
	    runtimeFlags |= Zygote.PROFILE_FROM_SHELL;  
	}  
	if (isProfileable) {  
	    runtimeFlags |= Zygote.PROFILEABLE;  
	} //把标志位信息保存到runtimeFlags中
	...//其他一些flag写入到runtimeFlags中去
	if (debuggableFlag) {  
    //debuggable时候使用wrap.sh去fork进程
	    String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";  
	    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();  
	    try {  
	        if (new File(wrapperFileName).exists()) {  
	            invokeWith = "/system/bin/logwrapper " + wrapperFileName;  
	        }  
	    } finally {  
	        StrictMode.setThreadPolicy(oldPolicy);  
	    }  
	}
	String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;  
	if (requiredAbi == null) {  //设置 app native 库使用的abi,如arm或者x86或者armv8等等
	    requiredAbi = Build.SUPPORTED_ABIS[0];  
	}
	String instructionSet = null;  
	if (app.info.primaryCpuAbi != null) {  
	    instructionSet = VMRuntime.getInstructionSet(requiredAbi);  
	}  
  
	app.setGids(gids);  
	app.setRequiredAbi(requiredAbi);  
	app.setInstructionSet(instructionSet); //把信息都设置到ProcessRecord中
	final String seInfo = app.info.seInfo  
        + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);  
	final String entryPoint = "android.app.ActivityThread";   //设置进程入口位ActivityThread
  
	return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,  
        runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,  
        instructionSet, invokeWith, startUptime, startElapsedTime);
	
}

以上代码主要是检查应用的各种权限,对其设置对应权限组的groupId,以及设置应用的Abi等信息。之后又会启动一个新的startProcessLocked方法,其中仍然是给ProcessRecord设置参数,其中很大篇幅的为设置debug和profilable相关的参数设置,这里就不列出参数设置的代码了,只列以下最后启动调用的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (mService.mConstants.FLAG_PROCESS_START_ASYNC) {
	mService.mProcStartHandler.post(() -> handleProcessStart(  
        app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,  
        requiredAbi, instructionSet, invokeWith, startSeq));
    return true;
} else {
	final Process.ProcessStartResult startResult = startProcess(hostingRecord,  
        entryPoint, app,  
        uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo,  
        requiredAbi, instructionSet, invokeWith, startUptime);  
	handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper,  
        startSeq, false);
	return app.getPid() > 0;
}

这里有两个分支,这个 FLAG_PROCESS_START_ASYNC 默认为True,是通过系统的Setting去设置的。第一个分支是通过Handle把任务抛出去执行,而直接返回了执行成功,另一个分支则是等待任务执行完成,在根据返回的UID检查是否成功。不过两个分支里面都是执行了startProcess方法,在这个方法中我们关注以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (hostingRecord.usesWebviewZygote()) {  //webview进程的创建
    startResult = startWebView(entryPoint,  
            app.processName, uid, uid, gids, runtimeFlags, mountExternal,  
            app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,  
            app.info.dataDir, null, app.info.packageName,  
            app.getDisabledCompatChanges(),  
            new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});  
} else if (hostingRecord.usesAppZygote()) {  
    final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);  
  
    startResult = appZygote.getProcess().start(entryPoint,  
            app.processName, uid, uid, gids, runtimeFlags, mountExternal,  
            app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,  
            app.info.dataDir, null, app.info.packageName,  
            /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,  
            app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap,  
            false, false,  
            new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});  
} else {  
    regularZygote = true;  
    startResult = Process.start(entryPoint,  
            app.processName, uid, uid, gids, runtimeFlags, mountExternal,  
            app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,  
            app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,  
            isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap,  
            allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs,  
            new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});  
}

以上可以看到我们在创建新的进程的时候,会有三个分支,我们回看我们创建HostingRecord 时候是调用的如下的构造方法:

1
2
3
4
5
public HostingRecord(@NonNull String hostingType, ComponentName hostingName, boolean isTopApp) {  
    this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,  
            null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */,  
            null /* definingProcessName */, null /* action */, TRIGGER_TYPE_UNKNOWN);  
}

Process启动进程调用

因此上面的代码是走到了regular分支,它调用了Processstart方法, Process中又调用了ZYGOTE_PROCESSstart方法, ZYGOTE_PROCESS为一个ZygoteProcess常量,其中又会调用startViaZygoate方法,我们来看看这个方法的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private Process.ProcessStartResult startViaZygote(@NonNull final String processClass,  @Nullable final String niceName,  final int uid, final int gid,  @Nullable final int[] gids,  
    int runtimeFlags, int mountExternal,  
     int targetSdkVersion,  
     @Nullable String seInfo,  
      @NonNull String abi,  
     @Nullable String instructionSet,  
     @Nullable String appDataDir,  
      @Nullable String invokeWith,  
    boolean startChildZygote,  
     @Nullable String packageName,  
      int zygotePolicyFlags,  
     boolean isTopApp,  
    @Nullable long[] disabledCompatChanges,  
      @Nullable Map<String, Pair<String, Long>>  pkgDataInfoMap,  
       @Nullable Map<String, Pair<String, Long>>  allowlistedDataInfoList,  
    boolean bindMountAppsData,  
    boolean bindMountAppStorageDirs,  
     @Nullable String[] extraArgs)  throws ZygoteStartFailedEx {  
    ArrayList<String> argsForZygote = new ArrayList<>();
    argsForZygote.add("--runtime-args");  
	argsForZygote.add("--setuid=" + uid);  
	argsForZygote.add("--setgid=" + gid);  
	argsForZygote.add("--runtime-flags=" + runtimeFlags);
	....
	synchronized(mLock) {  
	    return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),  
                                      zygotePolicyFlags,  
                                      argsForZygote);  
}   

上面的代码就是把我们之前所有的各种参数,都拼接起来放到一个字符数组中,后面的openZygoteSocketIfNeeded则是根据abi来于zygote进程建立socket连接,其他的我就要进入zygoteSendArgsAndGetResult方法中查看详情了。

1
2
3
4
5
if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {  
    return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);  
}  
  
return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);

这里有一个判断是否要使用usap进程池(非专门app使用进程池),不过我看了这里mUsapPoolEnabled字段默认为false,那我们就不看这个分支了。而attemptZygoteSendArgsAndGetResult代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult(  
        ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx {  
    try {  
        final BufferedWriter zygoteWriter = zygoteState.mZygoteOutputWriter;  
        final DataInputStream zygoteInputStream = zygoteState.mZygoteInputStream;  
  
        zygoteWriter.write(msgStr);  
        zygoteWriter.flush();  
  
        Process.ProcessStartResult result = new Process.ProcessStartResult();  
        result.pid = zygoteInputStream.readInt();  
        result.usingWrapper = zygoteInputStream.readBoolean();  
  
        if (result.pid < 0) {  
            throw new ZygoteStartFailedEx("fork() failed");  
        }  
  
        return result;  
    } catch (IOException ex) {  
        zygoteState.close();  
        Log.e(LOG_TAG, "IO Exception while communicating with Zygote - "  
                + ex.toString());  
        throw new ZygoteStartFailedEx(ex);  
    }  
}

从上面的代码我们可以看到,这里其实很简单,就是通过socket向Zytgote发送了我们启动进程需要的参数,然后再通过socket从Zygote读出创建的进程的pid。

Zygote进程创建子进程

这个时候我们需要来看ZygoteInit的main方法,具体zygote进程是如何在系统启动的时候创建的就不去关注了,这里来关注zygote进程如何去创建应用进程的,这里摘抄了一些它的main函数的代码:

1
2
3
4
5
6
7
preload(bootTimingsTraceLog); //zygote启动之后,预加载代码资源等
zygoteServer = new ZygoteServer(isPrimaryZygote); //创建Zygote 的socket server
caller = zygoteServer.runSelectLoop(abiList); // socket server进入监听状态

if (caller != null) {  
    caller.run();  //子进程中的时候caller不为空,会执行,此处会执行我们的ActivityThread的main方法,先分析上面的runSelectLoop,其中会有caller的创建
}

runSelectLoop内我们比较关注的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Runnable runSelectLoop(String abiList) {
	while (true) {
		pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
		if (pollReturnValue == 0) {
		 ...
		} else {
			while (--pollIndex >= 0) {
				if (pollIndex == 0) {
					//如果pollIndex为0,则说明没有socket连接,需要创建socket连接
					ZygoteConnection newPeer = acceptCommandPeer(abiList);  
					peers.add(newPeer);  
					socketFDs.add(newPeer.getFileDescriptor());
				} else if (pollIndex < usapPoolEventFDIndex) { //读取Primary socket
					ZygoteConnection connection = peers.get(pollIndex);  
					boolean multipleForksOK = !isUsapPoolEnabled()  && ZygoteHooks.isIndefiniteThreadSuspensionSafe();  
					final Runnable command =  connection.processCommand(this, multipleForksOK); 
					if  (mIsForkChild) {
						return command; //子进程,返回command
					} else {
						//父进程的一些处理
					}
					...
				}
				....
			}
			...
		}
		
	}
}

上面的代码省略了一些如果是Usap进程的代码,代码里面有两层的循环,在内层循环中,以pollIndex作为循环的条件,如果pollIndex为0,在acceptCommandPeer中会建立新的Socket Connet,代码里面就是一个ZygoteConnection。如果存在Connect的情况下会,会通过判断当前pollIndex是否小于usapPollEventFDIndex来判断是否是普通的进程创建,之后会调用connection.processCommand来读取socket数据做后续的处理,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ZygoteArguments parsedArgs;
try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) {
	while (true) {
		parsedArgs = ZygoteArguments.getInstance(argBuffer);
		...
		if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote  
        || !multipleOK || peer.getUid() != Process.SYSTEM_UID) {  
		    pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,  
	            parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,  
	            parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,  
	            fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,  
	            parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,  
	            parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,  
	            parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,  
	            parsedArgs.mBindMountAppStorageDirs);  //fork子进程的操作
  
		    try {  
		        if (pid == 0) {  //子进程处理分支
		            zygoteServer.setForkChild();  
  
		            zygoteServer.closeServerSocket();  
		            IoUtils.closeQuietly(serverPipeFd);  
		            serverPipeFd = null;  
  
		            return handleChildProc(parsedArgs, childPipeFd,  
                    parsedArgs.mStartChildZygote);  
		        } else {  //父进程处理分支
			        IoUtils.closeQuietly(childPipeFd);  
					childPipeFd = null;  
					handleParentProc(pid, serverPipeFd);
			        return null;
		        }   
		    } finally {  
		        IoUtils.closeQuietly(childPipeFd);  
		        IoUtils.closeQuietly(serverPipeFd);  
		    }  
		} else {
			...
		}
		...
	}
}

上面代码是处理socket数据的代码,我这里省略了除了创建进程之外的处理其他操作的代码。其中我们可以看到系统是使用了ZygoteArguments来解析我们之前从system_server进程传过来的参数,之后调用Zygote.forkAndSpecialize来创建进程,在linux中,fork完进程之后,是通过pid来判断当前是在父进程还是子进程中的,当前为子进程则pid为0。forkAndSpecialize方法中主要是调用了nativeForkAndSpecialize,这个是native方法,代码在com_android_internal_os_Zygote.cpp中,在native中的方法为com_android_internal_os_Zygote_nativeForkAndSpecialize我们去看看它的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
pid_t pid = zygote::ForkCommon(env, /* is_system_server= */ false, fds_to_close, fds_to_ignore,  true);
if (pid == 0) {  
    SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,  
                     mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,  
                     instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,  
                     allowlisted_data_info_list, mount_data_dirs == JNI_TRUE, 
                     mount_storage_dirs == JNI_TRUE);  
}
return pid;

上面的代码可以看到,第一行是去fork子进程,后面会判断是否为子进程,如果为子进程则会为子进程做一些处理。其中我省略了前面一部分fds_to_close 和fds_to_ignore赋值的代码,那些为需要关闭或者忽略的文件描述符,会传到这个ForkCommon方法中,我们具体看看这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
SetSignalHandlers(); //设置错误信号监听
BlockSignal(SIGCHLD, fail_fn);  //暂时关闭SIGCHLD信号,方便后面关闭fd
__android_log_close();  //关闭log相关的FD
AStatsSocket_close();
...
pid_t pid = fork(); //调用系统调用执行fork进程

if (pid == 0) {
	...
	PreApplicationInit(); //子进程的初始化,主要是设置当前进程不是zygote进程
	DetachDescriptors(env, fds_to_close, fail_fn); //把传进来的要关闭的fd关掉
	...
} else {
	...
}

UnblockSignal(SIGCHLD, fail_fn); //重新打开之前关闭的SIGCHLD信号
return pid;

可以看到上面的代码主要是去调用fork系统调用去从zygote进程fork一个新进程作为应用使用的进程,而SpecializeCommon,我们根据传入的参数和代码可以知道,其中主要是设置子进程的用户组,以及挂载应用目录,一些其他相关的初始化,就不分析其代码了。然后我们就可以继续会到java代码。

创建完子进程后的操作

在前面processCommand方法中,我们知道fork成功之后如果是子进程会执行handleChildProc方法,如果是父进程会执行handleParentProc方法,先来看一下父进程执行的代码:

1
2
3
4
5
6
if (pid > 0) {  
    setChildPgid(pid);  
}
...
mSocketOutStream.writeInt(pid);  
mSocketOutStream.writeBoolean(usingWrapper);

这个方法中我们需要关注的就上面这一部分代码,首先是把这个子进程的pid放到进程的当前进程的孩子进程组中去。后面的就是把子进程的pid和是否使用了wrapper写入到socket中,这样我们之前请求创建进程那个地方就能拿到子进程的id了。

再来看子进程所执行的handleChildProc方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
closeSocket();
Zygote.setAppProcessName(parsedArgs, TAG);

if (parsedArgs.mInvokeWith != null) {  
    WrapperInit.execApplication(parsedArgs.mInvokeWith,  
            parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,  
            VMRuntime.getCurrentInstructionSet(),  
            pipeFd, parsedArgs.mRemainingArgs);  
  
    // Should not get here.  
    throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");  
} else {  
    if (!isZygote) {  
        return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,  
                parsedArgs.mDisabledCompatChanges,  
                parsedArgs.mRemainingArgs, null /* classLoader */);  
    } else {  
        return ZygoteInit.childZygoteInit(  
                parsedArgs.mRemainingArgs  /* classLoader */);  
    }  
}

首先第一行是关闭socket,前面的native代码其实已经关闭过了socket,但是在java层还是有LocalSocket,也需要关闭。 第二行就是给我们这个进程设置名称。 后面的第一个判断是看我们是否使用wrapper,正常流程不会走到这里,else分支中我们这里也不是fork一个新的zygote进程,因此也只需要看ZygoteInit.zygoteInit这个方法即可。

1
2
3
4
5
6
RuntimeInit.redirectLogStreams();  //关闭默认的log,设置使用android的print来输出system.out和system.error的log
  
RuntimeInit.commonInit();   
ZygoteInit.nativeZygoteInit();  
return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,  
        classLoader);

App进程的初始化

RuntimeInit.commonInit中是一些初始化,包括错误处理,时区,网络的userAgent等,不看代码了。nativeZygoteInit的代码在AndroidRuntime.cpp

1
2
3
4
static void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)  
{  
    gCurRuntime->onZygoteInit();  
}

此处调用了gCurRuntimeonZygoteInit()方法,而这个方法是AndroidRuntime中的一个虚方法,在app_main.cpp中我们看到实际上对于应用我们有一个子类AppRuntime中实现了这个方法,代码如下:

1
2
3
4
5
6
virtual void onZygoteInit()  
{  
    sp<ProcessState> proc = ProcessState::self();  
    ALOGV("App process: starting thread pool.\n");  
    proc->startThreadPool();  
}

我们之前分析binder的时候,知道ProcessState这个类binder是有使用的,调用self方法会打开binder驱动,这个代码里面是为binder创建应用进程的线程池,具体这里就不分析了。继续看RuntimeInit.applicationInit代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,  
        String[] argv, ClassLoader classLoader) {  
    nativeSetExitWithoutCleanup(true);  
  
    VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);  
    VMRuntime.getRuntime().setDisabledCompatChanges(disabledCompatChanges);  
  
    final Arguments args = new Arguments(argv);  
  
    return findStaticMain(args.startClass, args.startArgs, classLoader);  
}

上面的方面,前面的代码主要是设置targetSdkversion和其他的一些设置,我们主要来看后面的findStaticMain方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected static Runnable findStaticMain(String className, String[] argv,  
        ClassLoader classLoader) {  
    Class<?> cl;  
  
    try {  
        cl = Class.forName(className, true, classLoader);  
    } catch (ClassNotFoundException ex) {  
        throw new RuntimeException(  
                "Missing class when invoking static main " + className,  
                ex);  
    }  
  
    Method m;  
    try {  
        m = cl.getMethod("main", new Class[] { String[].class });  
    } catch (NoSuchMethodException ex) {  
        throw new RuntimeException(  
                "Missing static main on " + className, ex);  
    } catch (SecurityException ex) {  
        throw new RuntimeException(  
                "Problem getting static main on " + className, ex);  
    }  
  
    int modifiers = m.getModifiers();  
    if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {  
        throw new RuntimeException(  
                "Main method is not public and static on " + className);  
    }  
   return new MethodAndArgsCaller(m, argv);  
}

可以看到我们通过反射拿到应用的之前设置的应用入口,也就是ActivityThread类,之后再获取到它的main方法,最后组装成一个MethodAndArgsCaller对象,最后返回。从前面的代码我们知道它会在ZygoteInitmain方法中执行。然后我们就可以来分析ActivityThread代码了。

ActivityThread代码执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
	AndroidOs.install();
	Environment.initForCurrentUser();
	final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());  
	TrustedCertificateStore.setDefaultUserDirectory(configDir);
	initializeMainlineModules();

	Looper.prepareMainLooper();
	...
	ActivityThread thread = new ActivityThread();  
	thread.attach(false, startSeq);
	Looper.loop();
	throw new RuntimeException("Main thread loop unexpectedly exited");

}

上面的代码可以看到是为应用进程做一些初始化,首先是为sys call使用android的一些定制,其次是指定CA证书的位置,之后安装Mainline的模块,后面是初始化looper进入Looper循环,这样应用的主线程也就完成了初始化。在启动loop之前有一个attach方法,对于应用进程我们传进来的第一个参数为false, 也就是非系统进程,我们来看代码,只看应用进程的分支。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
RuntimeInit.setApplicationObject(mAppThread.asBinder());  
final IActivityManager mgr = ActivityManager.getService();  
try {  
    mgr.attachApplication(mAppThread, startSeq);  
} catch (RemoteException ex) {  
    throw ex.rethrowFromSystemServer();  
}

BinderInternal.addGcWatcher(new Runnable() {
	public void run() {
		//监听gc,当可用内存比较小的时候尝试回收一些Activity
	}
}

ViewRootImpl.ConfigChangedCallback configChangedCallback = (Configuration globalConfig) -> {
	//config 变化的回调,用来更新app得而configuration
}
ViewRootImpl.addConfigCallback(configChangedCallback);

上面的代码主要做了四件事情,其中两个是注册gc的回调和view configration 变化的回调,我们最关注的是调用ActivityManager的atttachApplication,和RuntimeInit的setApplicationObject,它们都用到了mAppThread,这个对象为ApplicationThread,而它是IApplicationThread.aidl的客户端实现。这里首先是把它的IBinder对象传到RuntimeInit中,这样发生一些事情的时候系统可以通知到应用。

回到AMS

另外我们再来看一下ActivityManager的attchApplication方法,它实际调用的是ActivityManagerServiceattachApplication方法,在它内部又调用了attachApplicationLocked方法,这里只看一下我们比较关心的一部分代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);  
List<ProviderInfo> providers = normalMode  
                                    ? mCpHelper.generateApplicationProvidersLocked(app)  
                                    : null;
...
final ProviderInfoList providerList = ProviderInfoList.fromList(providers);
...
thread.bindApplication(processName, appInfo,  
        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,  
        providerList, null, profilerInfo, null, null, null, testMode,  
        mBinderTransactionTrackingEnabled, enableTrackAllocation,  
        isRestrictedBackupMode || !normalMode, app.isPersistent(),  
        new Configuration(app.getWindowProcessController().getConfiguration()),  
        app.getCompat(), getCommonServicesLocked(app.isolated),  
        mCoreSettingsObserver.getCoreSettingsLocked(),  
        buildSerial, autofillOptions, contentCaptureOptions,  
        app.getDisabledCompatChanges(), serializedSystemFontMap,  
        app.getStartElapsedTime(), app.getStartUptime()); 
...
if (normalMode) {  
    try {  
        didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());   
    } catch (Exception e) {  
        Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);  
        badApp = true;  
    }  
}
if (!badApp) {  
    try {  
        didSomething |= mServices.attachApplicationLocked(app, processName);  
        checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked");   //检查是否有Service需要在当前进程启动
    } catch (Exception e) {  
        Slog.wtf(TAG, "Exception thrown starting services in " + app, e);  
        badApp = true;  
    }  
}

if (!badApp && isPendingBroadcastProcessLocked(pid)) {  
    try {  
        didSomething |= sendPendingBroadcastsLocked(app);   //发送pending的广播
        checkTime(startTime, "attachApplicationLocked: after sendPendingBroadcastsLocked");  
    } catch (Exception e) {  
        // If the app died trying to launch the receiver we declare it 'bad'  
        Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e);  
        badApp = true;  
    }  
}

上面省略了一些代码,不过我们application需要做的一些核心代码都还在。除了列出的代码外,这里其实还有一些pending service启动,pending 广播的执行,以及ContentProvider的安装等,这些我们先略过。 首先这个normalMode的判断,我们假设当前已经是使用中而不是刚启动手机,而mProcessesReady是在system_server启动之后就赋值为true了,所以对于app启动的状况来说,这里normalMode为true。这里我们需要重点关注的就两个地方,一个是thread.bindApplication,另一处是mAtmInternal.attachApplication。bindApplication会通过binder调用到应用进程的bindApplication方法。

1
2
3
4
AppBindData data = new AppBindData();  
data.processName = processName;
....
sendMessage(H.BIND_APPLICATION, data);

这里主要就去构建了AppBindData,使用ActivityThread内部的H来发送消息,消息回调处会调用ActivityThread的handleBindApplication方法。这个方法的代码非常多,前面的一些是设置包名,进程名称等等信息,以及configration信息以及调试器相关的东西,这些我们都不关注,这里跳过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo, isSdkSandbox);   //构建apk信息,创建LoadApk对象。

//创建AppContent,并且把confirmration绑定到Context上
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);  
mConfigurationController.updateLocaleListFromAppContext(appContext);

//创建Instrumentation
mInstrumentation = new Instrumentation();  
mInstrumentation.basicInit(this);

if (!data.restrictedBackupMode) {   //执行安装ContentProvider
    if (!ArrayUtils.isEmpty(data.providers)) {  
        installContentProviders(app, data.providers);  
    }  
}

//创建程序的Application
app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
//调用Application的onCreate方法
mInstrumentation.callApplicationOnCreate(app);

上面的逻辑我们需要关注makeApplicationInner, 后面的callApplicationOnCreate内部就是调用Application的onCreate方法,不再分析了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final Application cached = sApplications.get(mPackageName);  
if (cached != null) {
	if (!allowDuplicateInstances) {  
	    mApplication = cached;  
	    return cached;  
	}
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(  
        cl, appClass, appContext);  
appContext.setOuterContext(app);

上面的代码主要是从缓存里面取Application,如果没有则通过Instrumentaion去创建新的Applicaion,我们继续去看newApplication的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public Application newApplication(ClassLoader cl, String className, Context context)  
        throws InstantiationException, IllegalAccessException,   
        ClassNotFoundException {  
	String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(  
        myProcessName);
    Application app = getFactory(context.getPackageName())  
            .instantiateApplication(cl, className);  
    app.attach(context);  
    return app;  
}

这里的代码比较简单,就是拿到AppComponentFactory然后通过反射创建App的Application对象,之后调用app的attach方法,attach方法内部会调用attachBaseContext方法。就不往里去看代码了。

对于需要启动Activity的情况,我们需要看ActivityManagerServiceattachApplication,我们需要再看一下mAtmInternal.attachApplication。它会调用ActivityTaskManagerService的内部类LocalService的方法,内部会调用mRootWindowContainer.attachApplication(wpc);,它的内部又会调用mAttachApplicationHelper.process(app),内部又会调用ensureActivitiesVisible方法,一路看进去最终会调用EnsureActivitiesVisibleHelperprocess方法,它的内部会调用setActivityVisibilityState

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (!r.attachedToProcess()) {  
    makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,  
            resumeTopActivity && isTop, r);  
} else if (r.isVisibleRequested()) {  
    // If this activity is already visible, then there is nothing to do here.  
    if (DEBUG_VISIBILITY) {  
        Slog.v(TAG_VISIBILITY, "Skipping: already visible at " + r);  
    }  
  
    if (r.mClientVisibilityDeferred && mNotifyClients) {  
        r.makeActiveIfNeeded(r.mClientVisibilityDeferred ? null : starting);  
        r.mClientVisibilityDeferred = false;  
    }  
  
    r.handleAlreadyVisible();  
    if (mNotifyClients) {  
        r.makeActiveIfNeeded(mStarting);  
    }  
} else {  
    r.makeVisibleIfNeeded(mStarting, mNotifyClients);  
}

这些就是去执行启动Activity相关的逻辑,这里也先略过。

以下是AMS发起创建新进程的时序图:

sequenceDiagram
AMS->>+ProcessList: startProcessLocked
ProcessList->>ProcessList: newProcessRecordLocked
ProcessList->>ProcessList: startProcess
ProcessList->>+ZygoteProcess: startViaZygote
ZygoteProcess->>ZygoteProcess: openZygoteSocketIfNeeded
ZygoteProcess->>ZygoteProcess: zygoteSendArgsAndGetResult
ZygoteProcess->>ZygoteServer: send args and get Result 
ZygoteProcess-->>-ProcessList: return result with pid
ProcessList-->>-AMS: return ProcessRecord

以下是Zygote侧处理fork进程请求的时序图:

sequenceDiagram
ZygoteInit->>ZygoteServer: runSelectLoop
loop 无限循环
ZygoteServer->>ZygoteConnection: processCommand
ZygoteConnection->>Zygote: forkAndSpecialize
ZygoteConnection-->>ZygoteConnection: (Parent): notify child pid by socket
ZygoteServer-->>ZygoteInit: return command(child process)
end
rect rgb(191, 223, 255)
note right of ZygoteInit: fork完子进程执行的内容
ZygoteConnection->>ZygoteInit: zygoteInit
ZygoteInit->>ZygoteInit: nativeZygoteInit
ZygoteInit->>AppRuntime: onZygoteInit
ZygoteInit->>RuntimeInit: findStaticMain
RuntimeInit-->>ZygoteInit: return ActivityThread Main function caller
ZygoteInit->>ActivityThread: main
ActivityThread->>AMS: attachApplication
AMS->>ActivityThread: bindApplication
end

以上就是应用进程启动的完整流程,为了使得流程更加简洁,其中不太重要的步骤有作省略。如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

Android Binder源码分析:AIDL及匿名服务传输

2024-09-07 17:57:32

前面介绍的通过ServiceManager添加服务和获取服务,这些服务都是有名称的,我们可以通过ServiceManager来获取它。除此之外Android系统中还有一类Binder服务是匿名它,它们如何让客户端获得代理对象,并且使用呢,本文就一探究竟。

AIDL 介绍

AIDL全称为Android接口定义语言,是Android系统提供的一款可供用户用来抽象IPC的工具,它提供了语法让我们来定义跨进程通讯的服务接口,也就是.aidl文件,它也提供了工具,帮助我们把定义文件专程目标语言的代码。

我们自己使用AIDL创建的服务,或者一部分系统内的服务,比如IWindowSessionIApplicationThread等,这些多是运行在App进程,一般都不是系统服务,因此都是匿名的。我们这里以IApplicationThread来分析AIDL创建的匿名服务是怎么传递Binder给使用端的。

IApplicationThread来分析,则服务端是我们的App进程,而客户端则是system_server进程。作为AIDL创建的Binder,首先会有一个AIDL文件,这里是IApplicationThread.aidl,其中定义了一些跨进程调用的方法,部分内容如下:

package android.app;
...
oneway interface IApplicationThread {  
    void scheduleReceiver(in Intent intent, in ActivityInfo info,  
            in CompatibilityInfo compatInfo,  
            int resultCode, in String data, in Bundle extras, boolean sync,  
            int sendingUser, int processState);  
    @UnsupportedAppUsage  
    void scheduleStopService(IBinder token);
	...
}

当前Android AIDL已经支持生成Java、C++、Rust的代码,对于IApplicationThread这里我们只需关注生成Java版本的代码即可。生成的代码在IApplicationThread当中,大概如下所示:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
public interface IApplicationThread extends android.os.IInterface {
public static class Default implements android.app.IApplicationThread {
	@Override
	public void scheduleReceiver(android.content.Intent 
							...) throws android.os.RemoteException {}

	@Override
	public android.os.IBinder asBinder() {
		return null;
	}
}
  
public abstract static class Stub extends android.os.Binder
	implements android.app.IApplicationThread {
	public Stub() {
		this.attachInterface(this, DESCRIPTOR);
	}

	public static android.app.IApplicationThread asInterface(android.os.IBinder obj) {
		if ((obj == null)) {
			return null;
		}
		android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
		if (((iin != null) && (iin instanceof android.app.IApplicationThread))) {
			return ((android.app.IApplicationThread) iin);
		}
		return new android.app.IApplicationThread.Stub.Proxy(obj);
	}

	@Override
	public android.os.IBinder asBinder() {
		return this;
	}

	public static java.lang.String getDefaultTransactionName(int transactionCode) {
		switch (transactionCode) {
		case TRANSACTION_scheduleReceiver:{
			return "scheduleReceiver";
		}
		case TRANSACTION_scheduleCreateService: {
			return "scheduleCreateService";
		}
		default: {
			return null;
		}
		}
	}

	public java.lang.String getTransactionName(int transactionCode) {
		return this.getDefaultTransactionName(transactionCode);
	}

	@Override
	public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
	throws android.os.RemoteException {
		java.lang.String descriptor = DESCRIPTOR;
		if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION
			&& code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
			data.enforceInterface(descriptor);
		}
		switch (code) {
		case INTERFACE_TRANSACTION:{
			reply.writeString(descriptor);
			return true;
		}
		}

		switch (code) {
		case TRANSACTION_scheduleReceiver:{
			android.content.Intent _arg0;
			_arg0 = data.readTypedObject(android.content.Intent.CREATOR);
			...
			int _arg8;
			_arg8 = data.readInt();
			data.enforceNoDataAvail();
			this.scheduleReceiver(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6, _arg7, _arg8);
			break;
		}
		...
		default:{
			return super.onTransact(code, data, reply, flags);
		}
		}
		return true;
	}

	private static class Proxy implements android.app.IApplicationThread {
		private android.os.IBinder mRemote;

		Proxy(android.os.IBinder remote) {
			mRemote = remote;
		}

		@Override
		public android.os.IBinder asBinder() {
			return mRemote;
		}

		public java.lang.String getInterfaceDescriptor() {
			return DESCRIPTOR;
		}

		@Override
		public void scheduleReceiver(
			android.content.Intent intent,
			...
			int processState) throws android.os.RemoteException {
			android.os.Parcel _data = android.os.Parcel.obtain();
			try {
				_data.writeInterfaceToken(DESCRIPTOR);
				...
				_data.writeTypedObject(intent, 0);
				boolean _status = mRemote.transact(
					Stub.TRANSACTION_scheduleReceiver, _data, null, android.os.IBinder.FLAG_ONEWAY);
			} finally {
				_data.recycle();
			}
		}
		...
	}

	public static final java.lang.String DESCRIPTOR = "android.app.IApplicationThread";
	static final int TRANSACTION_scheduleReceiver = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
	static final int TRANSACTION_scheduleCreateService = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
...
	public int getMaxTransactionId() {
		return 57;
	}
}	
...
public void scheduleReceiver(
android.content.Intent intent,
...
int processState)
throws android.os.RemoteException;

}

对于Java代码,AIDL会生成一个跟AIDL同名的接口,同时继承自IInterface,同时还会创建内部类,分别为Default和Stub。Default为默认实现,大多数情况下是没有的,因为我们自己会实现。而Stub为抽象类,我们自己实现的时候会使用它,它以及继承自Binder,AIDL工具帮助我们把Parcel读写相关的代码已经生成,我们只需要去继承它,实现业务逻辑即可。而Stub中还有一个内部类Proxy,这个类用于Binder服务的客户端使用。对于IApplicationThread的实现,在ActivityThread当中。

匿名服务的传输

那么ActivityServiceManager(之后简称AMS)是怎么拿到ApplicationThread的呢,ActivityThread的attach方法则是这一切的入口:

1
2
3
4
5
6
final IActivityManager mgr = ActivityManager.getService();  
try {  
    mgr.attachApplication(mAppThread, startSeq);  
} catch (RemoteException ex) {  
    throw ex.rethrowFromSystemServer();  
}

代码中,首先拿到AMS的binder客户端类,这里也就是IActivityManager$Stub$Proxy,因为它也用了AIDL,所以跟我们ApplicationThread的类是类似的,具体如何拿到的,这个之前ServiceManager分析getService的时候已经分析过了,这里不看了。我们可以看一下它的attachApplication方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override public void attachApplication(android.app.IApplicationThread app, long startSeq) throws android.os.RemoteException  
{  
  android.os.Parcel _data = android.os.Parcel.obtain();  
  android.os.Parcel _reply = android.os.Parcel.obtain();  
  try {  
    _data.writeInterfaceToken(DESCRIPTOR);  
    _data.writeStrongInterface(app);  
    _data.writeLong(startSeq);  
    boolean _status = mRemote.transact(Stub.TRANSACTION_attachApplication, _data, _reply, 0);  
    _reply.readException();  
  }  
  finally {  
    _reply.recycle();  
    _data.recycle();  
  }  
}

这里也是跟我们之前addService类似,把binder写入到Parcel中去,因为App进程这里相当于是ApplicationThread它的服务端,因此这里写入的type为BINDER_TYPE_BINDER,而调用mRemote.transact则为调用BinderProxy的同名方法,我们知道最终会调用到IPCThreadStatetransact方法,从而调用binder驱动。

类似于getService的方法,AMS所在的system_server进程会收到BR_TRANSACTION命令,在其中解析数据知道调用的是TRANSACTION_attachApplication这个业务命令,进而使用readStrongInterface来获取到binder的代理对象BinderProxy。具体代码在IActivityManager.StubonTransact中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case TRANSACTION_attachApplication:  
{  
  android.app.IApplicationThread _arg0;  
  _arg0 = android.app.IApplicationThread.Stub.asInterface(data.readStrongBinder());  
  long _arg1;  
  _arg1 = data.readLong();  
  data.enforceNoDataAvail();  
  this.attachApplication(_arg0, _arg1);  
  reply.writeNoException();  
  break;  
}

到这样调用AMS服务端的attachApplication的时候就能使用IApplicationThread所提供的方法了。

Binder驱动中binder节点的处理

但是看到这里,有个问题,就是我们是匿名的binder,驱动怎么是处理的呢。这个需要去看一下binder驱动的源码,不过就不具体看调用流程了,直接看生成创建node和handle部分的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int binder_translate_binder(struct flat_binder_object *fp,  
                                   struct binder_transaction *t,  
                                   struct binder_thread *thread)  
{  
    struct binder_node *node;  
    struct binder_proc *proc = thread->proc;  
    struct binder_proc *target_proc = t->to_proc;  
    struct binder_ref_data rdata;  
    int ret = 0;  
  
    node = binder_get_node(proc, fp->binder);  
    if (!node) {  
        node = binder_new_node(proc, fp);  
        if (!node)  
            return -ENOMEM;  
    }  
    ...  
    ret = binder_inc_ref_for_node(target_proc, node,  
                                  fp->hdr.type == BINDER_TYPE_BINDER,  
                                  &thread->todo, &rdata);  
    ...  
    if (fp->hdr.type == BINDER_TYPE_BINDER)  
        fp->hdr.type = BINDER_TYPE_HANDLE;  
    else  
        fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;  
    fp->binder = 0;  
    fp->handle = rdata.desc;  
    fp->cookie = 0;  
  
    ...  
    done:  
    binder_put_node(node);  
    return ret;  
}

可以看到对于通过Parcel调用经过binder驱动的binder对象,binder驱动都会给他们创建一个binder_node,并且为其设置handle,在传输到客户端的时候还会把type设置为BINDER_TYPE_HANDLE。 这样我们就对整给流程有所了解了,如果读者还想窥探更多的细节,则需要自行去阅读binder驱动的源码了。

开发者如何使用匿名服务

这上面介绍的部分是我们使用系统的服务来获取AIDL创建的服务,对于应用开发者来说有什么办法呢。我们可以通过AIDL+Service来实现,Android的四大组件之一的Service,它提供了通过bindService的方式来启动服务。在它的onBind中就可以返回IBinder,Android Framework会帮助我们调用操作代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void handleBindService(BindServiceData data) {  
    CreateServiceData createData = mServicesData.get(data.token);  
    Service s = mServices.get(data.token);  
    if (s != null) {  
        try {  
           ....
            try {  
                if (!data.rebind) {  
                    IBinder binder = s.onBind(data.intent);  
                    ActivityManager.getService().publishService(  
                            data.token, data.intent, binder);  
                } else {  
                    ...
                }  
            } catch (RemoteException ex) {  
                throw ex.rethrowFromSystemServer();  
            }  
        } catch (Exception e) {  
            ....
        }  
    }  
}

可以看到在ActivityThreadhandleBindService方法 中,我们在拿到Service所提供的IBinder之后,AMS会调用publishService,我们可以在ServiceConnection回调中拿到Binder的代理对象,之后就可以进行跨进程通讯了。

另外Android Framework还为我们提供了Messenger,其实现为Service+AIDL+Handler,让我们不用自己写AIDL,我们自己定义Service的时候使用Messenger和Handler就可以实现跨进程通信了。

总结

到此为止,我们已经分析了Binder服务管家ServiceManager的启动、使用ServiceManger添加服务和查找服务以及匿名服务的传递,在此过程中我们了解了进程是如何与Binder驱动交互的,以及binder调用过程中的会执行的方法等,我们对于Binder就有了一个全面的了解。在本文还简单介绍 了应用开发者如何使用Binder,有了这些基础,我们后面分析Android系统其他部分的代码就会更加容易了。当然关于Binder驱动的代码,BInder线程池的管理这两块还没有分析,读者感性确可以自行阅读,也可查看其他博主的文章。

如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

Android Binder源码分析:添加服务和获取服务解析

2024-09-06 19:22:29

通过ServiceManager添加服务和获取服务分别为addServicegetService,两者流程上其实是有一些类似的,其中我们可以看到binder通讯的全过程。为了让内容更有意义,添加服务选择从Java层的代码触发,获取服务则选择从Native层触发。

添加服务

我们以添加ActivityManagerService为例分析添加一个Service。首先先画个简单的流程图,介绍涉及到的类和调用的方法,不过步骤进行了省略,详细看后面的代码解析。

ServiceManagerProxy请求添加Service

其代码在ActivityManagerServicesetSystemProcess方法当中,具体调用如下:

1
2
ServiceManager.addService(Context.ACTIVITY_SERVICE, this, /* allowIsolated= */ true,  
        DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);

具体实现如下:

1
2
3
4
5
6
7
8
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)  
public static void addService(String name, IBinder service, boolean allowIsolated,  
        int dumpPriority) {  
    try {  
        getIServiceManager().addService(name, service, allowIsolated, dumpPriority);  
    } catch (RemoteException e) {  
    }  
}

其中getIServiceManager的代码我们已经分析过了,我们之前拿到的IServiceManager的实例为ServiceManagerProxy,这里可以直接去看它的这个方法:

1
2
3
4
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)  
        throws RemoteException {  
    mServiceManager.addService(name, service, allowIsolated, dumpPriority);  
}

这里调用了mServiceManager的同名方法,而这个成员变量的初始化如下:

1
mServiceManager = IServiceManager.Stub.asInterface(remote);

这里的remote就是我们之前构造函数传入的BinderProxy,而上面的函数后我们发获取到的对象则为IServiceMaanager.Stub.Proxy,我们可以看一下它的同名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Override public void addService(java.lang.String name, android.os.IBinder service, boolean allowIsolated, int dumpPriority) throws android.os.RemoteException  
{  
  android.os.Parcel _data = android.os.Parcel.obtain();  
  android.os.Parcel _reply = android.os.Parcel.obtain();  
  try {  
    _data.writeInterfaceToken(DESCRIPTOR);  
    _data.writeString(name);  
    _data.writeStrongBinder(service);  
    _data.writeBoolean(allowIsolated);  
    _data.writeInt(dumpPriority);  
    boolean _status = mRemote.transact(Stub.TRANSACTION_addService, _data, _reply, 0);  
    _reply.readException();  
  }  
  finally {  
    _reply.recycle();  
    _data.recycle();  
  }  
}

这里我们看到就是把设置的数据和binder写入到Parcel之后 调用transact。这里我们可以看一下Parcel首先是写入了InterfaceToken,也就是IServiceManager的描述符,其次才是其他内容。我们主要关注一下如何写入Binder的。其最终调用的方法在android_os_Parcel.cpp中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)  
{  
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);  
    if (parcel != NULL) {  
        const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));  
        if (err != NO_ERROR) {  
            signalExceptionForError(env, clazz, err);  
        }  
    }  
}

我们主要关注第5行,这里有一个ibinderForJavaObject,用于从javaobject中拿到binder的native对象,我们可以看一下其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj)  {  
    if (obj == NULL) return NULL;  
  
    // 如果是Binder实例
    if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {  
        JavaBBinderHolder* jbh = (JavaBBinderHolder*)  
            env->GetLongField(obj, gBinderOffsets.mObject);  
        return jbh->get(env, obj);  
    }  
    // 如果是BinderProxy实例
    if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) {  
        return getBPNativeData(env, obj)->mObject;  
    }  
    return NULL;  
}

BinderProxyNativeData* getBPNativeData(JNIEnv* env, jobject obj) {  
    return (BinderProxyNativeData *) env->GetLongField(obj, gBinderProxyOffsets.mNativeData);  
}

这个地方会判断我们的传过来的javaobject是Binder的实例还是BinderProxy的实例,前者对应Binder的服务端,后者对应的是客户端,我们刚刚传过来的AMS则是服务端。这里是从javaobject拿到mObject成员变量,对应native的类JavaBBinderHolder,最后调用它的get方法拿到JavaBinder对象。此处算是完成了我们Java层的Binder在Native层的对应对象的获取。现在就可以看看ParcelwriteStrongBinder方法了:

1
2
3
status_t Parcel::writeStrongBinder(const sp<IBinder>& val)  {  
    return flattenBinder(val);  
}

其中又调用了flattenBinder,这个方法比较长,我们先一点点的贴代码:

1
2
3
4
5
6
7
BBinder* local = nullptr;  
if (binder) local = binder->localBinder();  
if (local) local->setParceled();

if (isForRpc()) {
...
}

这里binder是我们刚刚的拿到的JavaBBinder,它的localBinder()实现如下:

1
2
3
BBinder* BBinder::localBinder()  {  
    return this;  
}

也就是说返回了自己。另外这里我们不是RPC,所以其中的代码我们不需要关注,继续看后面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
flat_binder_object obj;
if (binder != nullptr) {  
    if (!local) {  
       ...//如果我们传入的不是BBinder,而是BpBinder执行这里的逻辑,省略
    } else {  
        int policy = local->getMinSchedulerPolicy();  
        int priority = local->getMinSchedulerPriority();  
  
        if (policy != 0 || priority != 0) {  
            // override value, since it is set explicitly  
            schedBits = schedPolicyMask(policy, priority);  
        }  
        obj.flags = FLAT_BINDER_FLAG_ACCEPTS_FDS;  
        .... 
        obj.hdr.type = BINDER_TYPE_BINDER;  
        obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());  
        obj.cookie = reinterpret_cast<uintptr_t>(local);  
    }  
} else {  
    ...
}
obj.flags |= schedBits;

上面的代码主要是将binder的一些参数拍平放到flat_binder_object当中。其中binder是放置到cookie字段,binder的弱引用放到了binder字段。

1
2
3
4
status_t status = writeObject(obj, false);  
if (status != OK) return status;  
  
return finishFlattenBinder(binder);

这里才开始真正的把数据写入 ,可以先看看这个writeObject方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)  
{  
    const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;  
    const bool enoughObjects = mObjectsSize < mObjectsCapacity;  
    if (enoughData && enoughObjects) {  
restart_write:  
        *reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val; //把数据写如内存  
        ...
        // Need to write meta-data?  
        if (nullMetaData || val.binder != 0) {  
            mObjects[mObjectsSize] = mDataPos;  
            acquire_object(ProcessState::self(), val, this);  
            mObjectsSize++;  
        }  
        return finishWrite(sizeof(flat_binder_object));  //调整dataPos和当前DateSize
    }  
	...
    goto restart_write;  
}

上面主要就是把binder写入内存当中,其他的则是处理内存不足的情况,有申请内存的代码,这里我们无须关注。可以在看一下前面的finishFlattenBinder方法:

1
2
3
4
5
status_t Parcel::finishFlattenBinder(const sp<IBinder>& binder)  {  
    internal::Stability::tryMarkCompilationUnit(binder.get());  
    int16_t rep = internal::Stability::getRepr(binder.get());  
    return writeInt32(rep);  
}

这个方法主要为binder设置Repr,并且把值也写入到Parcel当中去,默认值为Level::SYSTEM,我们不再深入看其代码。

到这里大概就看完了Parcel写数据的代码了。可以看看transact方法,这里的mRemoteBinderProxy,它的transact方法中主要调用了一下代码:

1
2
3
4
5
final boolean result = transactNative(code, data, reply, flags);  
if (reply != null && !warnOnBlocking) {  
    reply.addFlags(Parcel.FLAG_IS_REPLY_FROM_BLOCKING_ALLOWED_OBJECT);  
}  
return result;

它的native实现在android_util_Binder.cpp中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,  
        jint code, jobject dataObj, jobject replyObj, jint flags) 
{  
    Parcel* data = parcelForJavaObject(env, dataObj);  //从java层的Parcel对象获取native层的Parcel对象
    ...
    Parcel* reply = parcelForJavaObject(env, replyObj);  
    ...
    IBinder* target = getBPNativeData(env, obj)->mObject.get(); //获取native层的BinderProxy对象 
    if (target == NULL) {  
        ...
        return JNI_FALSE;  
    }  
  
    status_t err = target->transact(code, *data, reply, flags);  
    ...
    return JNI_FALSE;
}

上面主要就 是获取native层的Parcel对象和Binder对象,并且调用binder的transact方法。这里的Binder对象是什么呢,回顾之前分析的javaObjectForIBinder方法,可知此处拿到的应该是BpBinder对象。我们就可以看它的代码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
status_t BpBinder::transact(  
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)  
{  
    // 只有binder或者的时候才能执行 
    if (mAlive) {  
        bool privateVendor = flags & FLAG_PRIVATE_VENDOR;  
        // 用户层的flag移除
        flags = flags & ~FLAG_PRIVATE_VENDOR;  
  
        if (code >= FIRST_CALL_TRANSACTION && code <= LAST_CALL_TRANSACTION) {  
            ... //Stability 相等判断,此处略过
        }  
  
        status_t status;  
        if (CC_UNLIKELY(isRpcBinder())) {  
            ...
        } else {  
            status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);  
        }  
        ....
        if (status == DEAD_OBJECT) mAlive = 0;  
  
        return status;  
    }  
  
    return DEAD_OBJECT;  
}

因为ServiceManager的id为0,此处binderHandle()拿到的值应为0。此处主要也是调用了IPCThreadStatetransact方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
status_t IPCThreadState::transact(int32_t handle,  
                                  uint32_t code, const Parcel& data,  
                                  Parcel* reply, uint32_t flags)  
{  
    status_t err;  
  
    flags |= TF_ACCEPT_FDS;  
    err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);  
  
    if (err != NO_ERROR) {  
        if (reply) reply->setError(err);  
        return (mLastError = err);  
    }  
  
    if ((flags & TF_ONE_WAY) == 0) {  
        if (reply) {  
            err = waitForResponse(reply);  
        } else {  
            Parcel fakeReply;  
            err = waitForResponse(&fakeReply);  
        }  
    } else {  
        err = waitForResponse(nullptr, nullptr);
    }  
  
    return err;  
}

这里主要调用了两个方法,分别是writeTransactionDatawaitForResponse,我们分别看一下。首先是writeTransactionData,它的第一个参数为BC_TRANSACTION,这是用于与Binder驱动交互的命令,除了这个之外还有其他一些,可以在binder.h当中找到。现在可以看writeTransactionData的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,  
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)  
{  
    binder_transaction_data tr;  
  
    tr.target.ptr = 0; /* Don't pass uninitialized stack data to a remote process */  
    tr.target.handle = handle;  
    tr.code = code;  
    tr.flags = binderFlags;  
    tr.cookie = 0;  
    tr.sender_pid = 0;  
    tr.sender_euid = 0;  
  
    const status_t err = data.errorCheck();  
    if (err == NO_ERROR) {  
        tr.data_size = data.ipcDataSize();  
        tr.data.ptr.buffer = data.ipcData();  
        tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);  
        tr.data.ptr.offsets = data.ipcObjects();  
    } else if (statusBuffer) {  
        ...
    } else {  
        return (mLastError = err);  
    }  
  
    mOut.writeInt32(cmd);  
    mOut.write(&tr, sizeof(tr));  
  
    return NO_ERROR;  
}

这个方法中所做的事情为,把我们传入的data和code以及要调用的binder的id等都放到binder_transaction_data中去,同时又把这个tr和调用binder驱动的命令BC_TRANSACTION一起写入到mOut当中去,这个mOut也是一个Parcel对象。到这里,数据都写完了,但是binder驱动在那里读取处理这个数据呢,我们继续看waitForResponse,前面我们因为有传reply过来,因此会调用到第17行的waitForResponse方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)  
{  
    uint32_t cmd;  
    int32_t err;  
  
    while (1) {  
        if ((err=talkWithDriver()) < NO_ERROR) break;  
        err = mIn.errorCheck();  
        if (err < NO_ERROR) break;  
        if (mIn.dataAvail() == 0) continue;  
  
        cmd = (uint32_t)mIn.readInt32();  
  
        switch (cmd) {  
        case BR_ONEWAY_SPAM_SUSPECT:  
            [[fallthrough]];  
        ......
        case BR_ACQUIRE_RESULT:  
            {  
                const int32_t result = mIn.readInt32();  
                if (!acquireResult) continue;  
                *acquireResult = result ? NO_ERROR : INVALID_OPERATION;  
            }  
            goto finish;  
  
        case BR_REPLY:  
            {  
                binder_transaction_data tr;  
                err = mIn.read(&tr, sizeof(tr));  
                if (err != NO_ERROR) goto finish;  
  
                if (reply) {  
                    if ((tr.flags & TF_STATUS_CODE) == 0) {  
                        reply->ipcSetDataReference(  
                            reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),  
                            tr.data_size,  
                            reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),  
                            tr.offsets_size/sizeof(binder_size_t),  
                            freeBuffer);  
                    } else {  
                        ....
                } else {  
                   ...
                    continue;  
                }  
            }  
            goto finish;  
  
        default:  
            err = executeCommand(cmd);  
            if (err != NO_ERROR) goto finish;  
            break;  
        }  
    }  
  
finish:  
    if (err != NO_ERROR) {  
        if (acquireResult) *acquireResult = err;  
        if (reply) reply->setError(err);  
        mLastError = err;  
    }  
  
    return err;  
}

talkWithDriver分析

这里开启了一个while的无限循环,首先调用talkWithDriver,看名字就知道是与Binder驱动进行交互,这里首先会看看这个方法有没有报错,没有报错又会检查mIn是否有报错。我们前面看到过mOut,这里又有mIn,它们是用来做什么的呢,我们看一下talkWithDriver,可以发现一些东西:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
status_t IPCThreadState::talkWithDriver(bool doReceive)  
{  
    if (mProcess->mDriverFD < 0) {  
        return -EBADF;  
    }  
  
    binder_write_read bwr;  
  
    const bool needRead = mIn.dataPosition() >= mIn.dataSize();  
    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;  
  
    bwr.write_size = outAvail;  
    bwr.write_buffer = (uintptr_t)mOut.data();  
  
    // This is what we'll read.  
    if (doReceive && needRead) {  
        bwr.read_size = mIn.dataCapacity();  
        bwr.read_buffer = (uintptr_t)mIn.data();  
    } else {  
        bwr.read_size = 0;  
        bwr.read_buffer = 0;  
    }  
  
    // 无数据需要读写,直接返回
    if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;  
  
    bwr.write_consumed = 0;  
    bwr.read_consumed = 0;  
    status_t err;  
    do {  
        
#if defined(__ANDROID__)  
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)  
            err = NO_ERROR;  
        else  
            err = -errno;  
#else  
        err = INVALID_OPERATION;  
#endif  
        if (mProcess->mDriverFD < 0) {  
            err = -EBADF;  
        }  
    } while (err == -EINTR);  

  
    if (err >= NO_ERROR) {  
        if (bwr.write_consumed > 0) {  
            if (bwr.write_consumed < mOut.dataSize())  
                ...
            else {  
                mOut.setDataSize(0);  
                processPostWriteDerefs();  
            }  
        }  
        if (bwr.read_consumed > 0) {  
            mIn.setDataSize(bwr.read_consumed);  
            mIn.setDataPosition(0);  
        }  
        ...
        return NO_ERROR;  
    }  
  
    return err;  
}

这个方法传参的默认值为true,也就是需要接受结果。在这里我们看到有一个新的数据结构binder_write_read,此处会把mOut中的数据指针写入到它的write_buffer当中,同时把mIn的数据指针写入到read_buffer中,此处的写指的是向binder驱动中写。随后我们看到是在一个循环当中调用系统调用ioctl来与binder驱动进行交互,这里使用循环的原因是,当我们调用这个系统调用的时候可能会遇到遇到中断,我们之前的调用未能执行,因此需要一直等待到执行为止。

到这里我们就分析完了添加Service调用端的所有代码,此时我们需要看一下ServiceManager服务端与Binder进行交互的代码。

ServiceManager服务端处理添加Service

我们之前分析ServiceManager启动的时候,知道最后会注册Looper的监听,当Binder驱动有消息的时候,BinderCallbak的handleEvent就会执行去处理,那么当我们在客户端请求添加Binder服务的时候,这里也会执行。这个方法中执行了如下代码:

1
IPCThreadState::self()->handlePolledCommands();

这里我们可以看一下详细的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
status_t IPCThreadState::handlePolledCommands()  
{  
    status_t result;  
    do {  
	    //读取缓存数据知道处理完成
        result = getAndExecuteCommand();  
    } while (mIn.dataPosition() < mIn.dataSize());  
	//减少binder的引用数量,此处也会和驱动交互
    processPendingDerefs();  
    //若有为执行的命令,全部执行
    flushCommands();  
    return result;  
}

此处我们主要关注getAndExecuteCommand方法,后面都已经加了注释,此处不需要详细关注。getAndExecuteCommand方法当中也是首先调用talkWithDriver方法,这个方法前面分析过了,不再分析,这样执行完之后,mIn当中就会拿到客户端请求传输过来的数据了,之后就从数据中拿取命令和数据进行执行,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
size_t IN = mIn.dataAvail();  
if (IN < sizeof(int32_t)) return result;  
cmd = mIn.readInt32();   //读取命令
pthread_mutex_lock(&mProcess->mThreadCountLock);  //为了增加线程计数上锁
mProcess->mExecutingThreadsCount++;  
if (mProcess->mExecutingThreadsCount >= mProcess->mMaxThreads &&  
        mProcess->mStarvationStartTimeMs == 0) {  
    mProcess->mStarvationStartTimeMs = uptimeMillis();  
}  
pthread_mutex_unlock(&mProcess->mThreadCountLock);  
  
result = executeCommand(cmd);  //执行命令
  
pthread_mutex_lock(&mProcess->mThreadCountLock);  
mProcess->mExecutingThreadsCount--;
if (mProcess->mWaitingForThreads > 0) {  
    pthread_cond_broadcast(&mProcess->mThreadCountDecrement);  
}  
pthread_mutex_unlock(&mProcess->mThreadCountLock);

代码很多,但是大多都是为了给binder线程计数增减的。我们主要去看一下executeCommand中的代码,该方法中代码很多,而我们在客户端执行的是BC_TRANSACTION,因此这里应该收到的是BR_TRANSACTION命令,因此只需要看该分支的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
BBinder* obj;  
RefBase::weakref_type* refs;  
status_t result = NO_ERROR;  
  
switch ((uint32_t)cmd) {
	...
	case BR_TRANSACTION:
		binder_transaction_data_secctx tr_secctx;  
		binder_transaction_data& tr = tr_secctx.transaction_data;
		result = mIn.read(&tr, sizeof(tr)); //读取binder携带过来的数据到tr中

		Parcel buffer;  
		//将数据的引用放入Parcel当中
		buffer.ipcSetDataReference(  
		    reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),  
		    tr.data_size,  
		    reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),  
		    tr.offsets_size/sizeof(binder_size_t), freeBuffer);
		//设置调用这的uid,pid,flag等信息
		mCallingPid = tr.sender_pid;  
		mCallingSid = reinterpret_cast<const char*>(tr_secctx.secctx);  
		mCallingUid = tr.sender_euid;  
		mLastTransactionBinderFlags = tr.flags;
		if (tr.target.ptr) {  //ServiceManager的binder无ptr
			//非serviceManager的binder,tr.cookie为本地的BBinder对象指针
		    if (reinterpret_cast<RefBase::weakref_type*>(  
	            tr.target.ptr)->attemptIncStrong(this)) {  
			        error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,  
                &reply, tr.flags);  
	        reinterpret_cast<BBinder*>(tr.cookie)->decStrong(this);  
		    } else {  
		        error = UNKNOWN_TRANSACTION;  
		    }  
  
		} else {  
		//ServiceManager使用the_context_object这个BBinder对象。
		    error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);  
		}
		if ((tr.flags & TF_ONE_WAY) == 0) {  
		    if (error < NO_ERROR) reply.setError(error);  
  
		    constexpr uint32_t kForwardReplyFlags = TF_CLEAR_BUF;  
		    sendReply(reply, (tr.flags & kForwardReplyFlags));  //写入回复
		} else {
		...
		}
		...
}

return result;

上面的代码已经做了省略,逻辑就是首先从mIn这块内存中拿到数据,并且放Parcel中,随后把uid,pid相关的属性设置到当前进程。之后是获取BBinder对象去执行transact方法,对于普通的binder,对于普通的binder,会ptr这个字段,并且tr.cookie就是本地的BBinder对象指针,而对于ServiceManager,这里就会使用在启动ServiceManager时候调用setTheContextObject所设置的BBinder对象,也就是服务端的ServiceManager。这里transact执行完成之后会调用sendReply将执行结果通过binder驱动传递回binder调用端,从而完成整个流程。这里先看transact,分析完再来分析sendReply

transact方法在BBinder类当中,在其中会调用onTransact方法,而到ServiceManager,它的onTransact的实现在BnServiceManager当中,这个类则是通过AIDL工具生成的。因为没有源码,根据经验我们这边可以知道它会调用ServiceManageraddService方法,而其中最重要的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mNameToService[name] = Service {  
    .binder = binder,  
    .allowIsolated = allowIsolated,  
    .dumpPriority = dumpPriority,  
    .debugPid = ctx.debugPid,  
};  
  
auto it = mNameToRegistrationCallback.find(name);  
if (it != mNameToRegistrationCallback.end()) {  
    for (const sp<IServiceCallback>& cb : it->second) {  
        mNameToService[name].guaranteeClient = true;  
        // permission checked in registerForNotifications  
        cb->onRegistration(name, binder);  
    }  
}

看代码可知道,这里把Binder放到Service结构体当中,随后放入mNameToService当中,mNameToService是一个map。而mNameToRegistrationCallback中为服务注册的回调,当注册完成之后会调用它的onRegistration方法。

前面我们还有一个sendReply方法我们还未分析,这里再看一下:

1
2
3
4
5
6
7
8
9
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags)  
{  
    status_t err;  
    status_t statusBuffer;  
    err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);  
    if (err < NO_ERROR) return err;  
  
    return waitForResponse(nullptr, nullptr);  
}

writeTransactionData当中就是把我们的reply打包成为binder_transaction_data写入mOut当中,这里的命令为BC_REPLY,执行完之后调用waitForResponse,其中会调用talkWithDriver来回应,之后便结束了服务端的相应。客户端随后可以读取客户端的mIn数据可以获取reply的数据。到这里就分析完了Service注册的流程。

获取服务(getService)分析

之前分析getIServiceManageraddService我们都是从java层的代码出发去往后走分析代码,而getService其实有一些地方跟他们是类似的,为了减少重复流程的分析,这里从Native层的使用场景出发。这里以获取ICameraService为例。

获取defaultServiceManager

我们的起点在frameworks/av/camera/ndk/impl/ACameraManager.cpp当中,调用代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const char*  kCameraServiceName  = "media.camera";
....

sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder;  
do {  
    binder = sm->getService(String16(kCameraServiceName));  
    if (binder != nullptr) {  
        break;  
    }  
    usleep(kCameraServicePollDelay);  
} while(true);

这里使用defaultServiceManager来拿到ServiceManager,其源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//frameworks/native/libs/binder/IServiceManager.cpp
sp<IServiceManager> defaultServiceManager()  
{  
    std::call_once(gSmOnce, []() {  
        sp<AidlServiceManager> sm = nullptr;  
        while (sm == nullptr) {  
            sm = interface_cast<AidlServiceManager>(ProcessState::self()->getContextObject(nullptr));  
            if (sm == nullptr) {  
                sleep(1); 
            }  
        }  
  
        gDefaultServiceManager = sp<ServiceManagerShim>::make(sm);  
    });  
  
    return gDefaultServiceManager;  
}

这个代码跟我们之前java层的代码比较类似,也是先拿ContentObject,而ServiceManagerShim相当于是native层的ServiceManager的代理。native层的代码因为不需要把对象转成Java的消耗,代码其实更加简单一点。这里我们拿到了ServiceManagerShim,就可以继续去看它的getService方法了。

请求getService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sp<IBinder> ServiceManagerShim::getService(const String16& name) const  
{  
    static bool gSystemBootCompleted = false;  
  
    sp<IBinder> svc = checkService(name);  
    if (svc != nullptr) return svc;  
  
    const bool isVendorService =  
        strcmp(ProcessState::self()->getDriverName().c_str(), "/dev/vndbinder") == 0;  
    constexpr int64_t timeout = 5000;  
    int64_t startTime = uptimeMillis();  
    // 如果是Vendor的服务,不能够访问系统的属性
    if (!gSystemBootCompleted && !isVendorService) {  
#ifdef __ANDROID__  
        char bootCompleted[PROPERTY_VALUE_MAX];  
        property_get("sys.boot_completed", bootCompleted, "0");  
        gSystemBootCompleted = strcmp(bootCompleted, "1") == 0 ? true : false;  
#else  
        gSystemBootCompleted = true;  
#endif  
    }  
    // 如果拿不到binder service就等待,系统服务和vendor时间有区分,直到超时才停止
    const useconds_t sleepTime = gSystemBootCompleted ? 1000 : 100;  
    int n = 0;  
    while (uptimeMillis() - startTime < timeout) {  
        n++;  
        usleep(1000*sleepTime);  
  
        sp<IBinder> svc = checkService(name);  
        if (svc != nullptr) {  
            return svc;  
        }  
    }  
    return nullptr;  
}

这里就是调用checkService去获取Service,源码如下:

1
2
3
4
5
6
7
8
sp<IBinder> ServiceManagerShim::checkService(const String16& name) const  
{  
    sp<IBinder> ret;  
    if (!mTheRealServiceManager->checkService(String8(name).c_str(), &ret).isOk()) {  
        return nullptr;  
    }  
    return ret;  
}

这里我们调用了mTheRealServiceManagercheckService方法,这个变量的实例为ServiceManager的BpBinder子类,也是由AIDL生成,其代码如下:

1
2
3
4
Parcel data, reply;
data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor()); data.writeString16(name); 
remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply); 
return reply.readStrongBinder();

这里跟之前分析addService的部分类似,只有最后多了一个readStrongBinder,addServicewriteStrongBinder到data,这里是读取binder调用返回的数据。后续流程也跟addService类似,这里就不再分析了。我们更关注这个binder我们是怎么拿到的,因此需要看三个地方的代码,一个是ServiceManger拿到binder并且写入到驱动给我们的过程,第二个地方是IPCThreadState当中接收数据的处理,最后就是通过readStrongBinder拿到binder的处理了。

客户端请求获取Binder服务的流程大概如下图所示:

sequenceDiagram
ServiceManagerShim->>ServiceManagerShim: defaultServiceManager()
ServiceManagerShim->>ServiceManagerShim:getService()
ServiceManagerShim->>ServiceManagerShim:checkService()
ServiceManagerShim->>+BpServiceManager:checkService()
BpServiceManager->>+BpBinder: transact()
BpBinder->>+IPCThreadState: transact()
IPCThreadState->>IPCThreadState: writeTransactionData()
IPCThreadState->>IPCThreadState: waitForResponse()
IPCThreadState->>IPCThreadState: talkWithDriver()
IPCThreadState->>+BinderDriver: ioctl(BC_TRANSACTION)
BinderDriver-->>-IPCThreadState: reply:BR_REPLY
IPCThreadState-->>-BpBinder: return resut
BpBinder-->>-BpServiceManager: return result
BpServiceManager->>BpServiceManager: readStrongBinder()
BpServiceManager-->>-ServiceManagerShim: return binder

ServiceManager服务端getService

前面分析addService我们已经知道服务端调用路径是BBinder.transcat–>BnServiceManager.onTransact–>ServiceManger.addService,这里的服务端也是类似,具体可以看下面的流程图。

sequenceDiagram
BinderDriver-->IPCThreadState:handlePolledCommands
loop mIn.dataPosition < mIn.dataSize (当输入数据未处理完)
IPCThreadState->>+IPCThreadState: getAndExecuteCommand
IPCThreadState->>IPCThreadState: executeCommand: BR_TRANSACTION
IPCThreadState->>BBinder: transact()
BBinder->>+BnServiceManager: onTransact()
BnServiceManager->>+ServiceManager: getService()
ServiceManager-->>-BnServiceManager: return Binder
BnServiceManager->>BnServiceManager: writeStrongBinder()
BnServiceManager-->-BBinder: return reply Parcel
BBinder-->>IPCThreadState: return reply
IPCThreadState->>+IPCThreadState: sendReply
IPCThreadState->>IPCThreadState:writeTransactionData
IPCThreadState->>+IPCThreadState:waitForResponse(null, null)
IPCThreadState->>IPCThreadState: talkWithDriver
IPCThreadState->>BinderDriver: ioctl:BC_REPLY
IPCThreadState-->>-IPCThreadState::
IPCThreadState-->-IPCThreadState: finishSendReply
end

我们就省略与Binder交互的许多代码,可以直接去看getService的代码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Status ServiceManager::getService(const std::string& name, sp<IBinder>* outBinder) {  
    *outBinder = tryGetService(name, true);  
    return Status::ok();  
}

sp<IBinder> ServiceManager::tryGetService(const std::string& name, bool startIfNotFound) {  
    auto ctx = mAccess->getCallingContext();  
  
    sp<IBinder> out;  
    Service* service = nullptr;  
    if (auto it = mNameToService.find(name); it != mNameToService.end()) {  
        service = &(it->second);  
  
        if (!service->allowIsolated) {  //是否允许多用户环境运行
            uid_t appid = multiuser_get_app_id(ctx.uid);  
            bool isIsolated = appid >= AID_ISOLATED_START && appid <= AID_ISOLATED_END;  
  
            if (isIsolated) {  
                return nullptr;  
            }  
        }  
        out = service->binder;  
    }  
  
    if (!mAccess->canFind(ctx, name)) {  //SELinux 权限检查
        return nullptr;  
    }  
  
    if (!out && startIfNotFound) { 
        tryStartService(name);  
    }  
  
    return out;  
}

ServiceManger中获取Binder就是从我们之前添加Service的时候的那个ServiceMap中查找,当查找后做一些权限检查,当找不到的情况下,因为我们传如的startIfNotFound,因此会调用tryStartService去启动对应的Service,其代码如下:

1
2
3
4
5
6
void ServiceManager::tryStartService(const std::string& name) {  
    std::thread([=] {  
        if (!base::SetProperty("ctl.interface_start", "aidl/" + name)) {  
            ...
    }).detach();  
}

代码很简单,就是启动了一个线程,其中设置系统的properties,系统便会尝试启动这个服务,具体我们这里就不分析了。

IPCThreadState接收数据处理

在服务端发送数据时候会调用binder执行BC_REPLY,而客户端后收到BR_REPLY命令,也就是会执行waitForResponse中的如下部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)  
{
....
case BR_REPLY:  
    {  
        binder_transaction_data tr;  
        err = mIn.read(&tr, sizeof(tr));  
        ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY");  
        if (err != NO_ERROR) goto finish;  
  
        if (reply) {  
            if ((tr.flags & TF_STATUS_CODE) == 0) {  
                reply->ipcSetDataReference(  
                    reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),  
                    tr.data_size,  
                    reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),  
                    tr.offsets_size/sizeof(binder_size_t),  
                    freeBuffer);  
            } else {  
	            ...
            }  
        } else {  
            ...
            continue;  
        }  
    }  
    goto finish;
.....
}

也就是执行上面的ipcSetDataReference,可以看一下其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void Parcel::ipcSetDataReference(const uint8_t* data, size_t dataSize,  
    const binder_size_t* objects, size_t objectsCount, release_func relFunc)  
{  

    freeData();  //初始化Parcel状态
  
    mData = const_cast<uint8_t*>(data);  
    mDataSize = mDataCapacity = dataSize;  
    mObjects = const_cast<binder_size_t*>(objects);  
    mObjectsSize = mObjectsCapacity = objectsCount;  
    mOwner = relFunc;  
  
    binder_size_t minOffset = 0;  
    for (size_t i = 0; i < mObjectsSize; i++) {  
        binder_size_t offset = mObjects[i];  
        if (offset < minOffset) {  
            
            mObjectsSize = 0;  
            break;  
        }  
        const flat_binder_object* flat  
            = reinterpret_cast<const flat_binder_object*>(mData + offset);  
        uint32_t type = flat->hdr.type;  
        if (!(type == BINDER_TYPE_BINDER || type == BINDER_TYPE_HANDLE ||  
              type == BINDER_TYPE_FD)) {  
            ....  
            break;  
        }  
        minOffset = offset + sizeof(flat_binder_object);  
    }  
    scanForFds();  
}

代码比较简单,主要就是把data传入Parcel中,但是除此之外我们需要关注一下传入的relFunc,传入的方法为freeBuffer,此方法的执行会在下一次调用freeData的时候执行,它的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void IPCThreadState::freeBuffer(Parcel* parcel, const uint8_t* data,  
                                size_t /*dataSize*/,  
                                const binder_size_t* /*objects*/,  
                                size_t /*objectsSize*/)  
{  
    ALOG_ASSERT(data != NULL, "Called with NULL data");  
    if (parcel != nullptr) parcel->closeFileDescriptors();  
    IPCThreadState* state = self();  
    state->mOut.writeInt32(BC_FREE_BUFFER);  
    state->mOut.writePointer((uintptr_t)data);  
    state->flushIfNeeded();  
}

看到这里,我们知道会调用binder发送这个BC_FREE_BUFFER命令,这样驱动内部会清理内存,这样就完成了Parcel和内存缓冲区的空间清理。

readStrongBinder

readStrongBinder和我们之前看过的writeStrongBinder应该是一个相反的过程,直接看代码:

1
2
3
4
5
status_t Parcel::readStrongBinder(sp<IBinder>* val) const  
{  
    status_t status = readNullableStrongBinder(val);  
    return status;  
}

上面的代码会调用readNullableStrongBinder,而其内部又会调用unflattenBinder,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
status_t Parcel::unflattenBinder(sp<IBinder>* out) const  
{  
    if (isForRpc()) {  
        ...  
        return finishUnflattenBinder(binder, out);  
    }  
  
    const flat_binder_object* flat = readObject(false);  
  
    if (flat) {  
        switch (flat->hdr.type) {  
            case BINDER_TYPE_BINDER: {  
                sp<IBinder> binder =  
                        sp<IBinder>::fromExisting(reinterpret_cast<IBinder*>(flat->cookie));  
                return finishUnflattenBinder(binder, out);  
            }  
            case BINDER_TYPE_HANDLE: {  
                sp<IBinder> binder =  
                    ProcessState::self()->getStrongProxyForHandle(flat->handle);  
                return finishUnflattenBinder(binder, out);  
            }  
        }  
    }  
    return BAD_TYPE;  
}

其中readObject为从Parcel中读取flat_binder_object对象,当请求的进程和服务在同一个进程时候,这里的type就是BINDER_TYPE_BINDER,当请求的进程和服务不在同一个进程则为BINDER_TYPE_HANDLE,因此我们这里是BINDER_TYPE_HANDLEgetStrongProxyForHandle我们之前在分析获取ServiceManager的时候已经分析过了,只不过那个地方handle为固定的0,而这里则是从驱动中传过来的值,最后我们会拿到一个BpBinder,也就完成了查找的过程。

分析完添加服务,查找服务,一直之前介绍的启动ServiceManager和获取ServiceManager基本上就把Binder除了驱动部分的东西都覆盖了。还剩下应用层应该如何使用Binder以及,我们的匿名binder是怎么查找的,这个留待下次在写。

如果你也对于Android系统源码感兴趣,欢迎与我交流。

看完评论一下吧

Android源码分析:ServiceManager启动代码解析

2024-09-05 20:41:29

之前已经分析过获取ServiceManager了,不过那是在使用端,在分析使用ServiceManager去获取服务或者添加服务的时候发现,我使用的Android Studio for Platform默认没有把ServiceManager的源码导入。并且同时我们不知道ServiceManager的服务端是怎么启动,怎么响应的,因此决定还是需要分析一下这块的代码。

首先简单画了一下启动的流程,如下图所示(Entry表示我们的调用入口,也就是后面所说的main函数):

sequenceDiagram
autonumber
Entry->>+ProcessState: initWithDriver
ProcessState->>ProcessState: init
ProcessState-->>-Entry: return ProcessState
Entry->>ServiceManager: new
Entry->>ServiceManager: addService(this)
Entry ->>IPCThreadState: setTheContentObject
Entry->>ProcessState: becomeContextManager
Entry->>Looper:prepare
Entry ->>Looper:addFd
loop 永不退出
Entry ->>Looper:pollAll
end

ServiceManager的启动是在系统启动的时候进行启动的,它的启动是使用Linux系统的服务启动方式进行配置,配置文件在servicemanager.rc当中,配置如下:

service servicemanager /system/bin/servicemanager  
    class core animation  
    user system  
    group system readproc  
    critical  
    onrestart restart apexd  
    ...
    task_profiles ServiceCapacityLow  
    shutdown critical

具体系统如何调用这个Service的我们这里不必关心,我们可以直接来看ServiceManager启动相关的代码,代码在frameworks/native/cmds/servicemanager目录下面。启动的逻辑在main.cppmain方法中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char** argv) {  
	...
    const char* driver = argc == 2 ? argv[1] : "/dev/binder";  
    sp<ProcessState> ps = ProcessState::initWithDriver(driver);  
    ps->setThreadPoolMaxThreadCount(0);  //设置最大线程数,因为serviceManager不使用`startThreadPool`启动线程池,因此设置为0
    ps->setCallRestriction(ProcessState::CallRestriction::FATAL_IF_NOT_ONEWAY);  
    sp<ServiceManager> manager = sp<ServiceManager>::make(std::make_unique<Access>());  
    if (!manager->addService("manager", manager, false /*allowIsolated*/, IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT).isOk()) {  
        LOG(ERROR) << "Could not self register servicemanager";  
    }  
  
    IPCThreadState::self()->setTheContextObject(manager);  
    ps->becomeContextManager();  
    sp<Looper> looper = Looper::prepare(false /*allowNonCallbacks*/);  
    BinderCallback::setupTo(looper);  
    ClientCallbackCallback::setupTo(looper, manager);  
    while(true) {  
        looper->pollAll(-1);  
    }  
    // 应该不会走到,除非发生了错误
    return EXIT_FAILURE;  
}

代码如上,数量不多,并且其中看到了不少熟悉的类名,首先是使用ProcessState类去调用它的initWithDriver,这里最终也会走到init方法,做的事情也是创建ProcessState实例,并且打开Binder驱动,获取驱动的FD,可以参考前面的文章。

第7行代码,我们可以看到,创建了一个ServiceManager对象,这个对象与我们之前分析的ServiceManager是不同的,它是ServiceManager的Binder服务端,这个代码也跟我们的初始化代码在同一个目录。拿到这个manager后面做的第一件事情就是调用addService把自己也加进去,addService的代码后面再来分析。

再看12行代码,setTheContextObject处传入了manager,这里IPCThreadState初始化代码之前也已经分析过,此处略过,这个方法也只是把manager作为它的成员放入,暂时略过,直接看后面的becomeContextManager源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
bool ProcessState::becomeContextManager()  
{  
    AutoMutex _l(mLock);  
    flat_binder_object obj {  
        .flags = FLAT_BINDER_FLAG_TXN_SECURITY_CTX,  
    };  
	//与Binder交互,发送命令BINDER_SET_CONTEXT_MGR_EXT,让当前进程成为Binder的上下文管理者
    int result = ioctl(mDriverFD, BINDER_SET_CONTEXT_MGR_EXT, &obj);  
    
    if (result != 0) {  
        android_errorWriteLog(0x534e4554, "121035042");  
  
        int unused = 0;  
        // 执行失败,则重置该参数
        result = ioctl(mDriverFD, BINDER_SET_CONTEXT_MGR, &unused);  
    }  
    ...
    return result == 0;  
}

上面的代码就是告诉binder驱动,当前的进程要成为binder的上下文管理者,驱动内部作何处理我们便不再深究。

继续看我们前面的代码,就出现了熟悉的Looper,这个Java层的Looper用法和功能都一样,preapre之后又在死循环中调用pollAll,从而我们的代码就会永远在这里执行而不会退出。但是Looper在这里有什么用处,我们需要看看前面的代码。首先是15行的调用,其源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static sp<BinderCallback> setupTo(const sp<Looper>& looper) {  
    sp<BinderCallback> cb = sp<BinderCallback>::make();  
  
    int binder_fd = -1;  
    IPCThreadState::self()->setupPolling(&binder_fd); 
  
    int ret = looper->addFd(binder_fd,  
                            Looper::POLL_CALLBACK,  
                            Looper::EVENT_INPUT,  
                            cb,  
                            nullptr /*data*/);  
    return cb;  
}

第5行代码调用的setupPolling代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
status_t IPCThreadState::setupPolling(int* fd)  
{  
    if (mProcess->mDriverFD < 0) {  //当前驱动没有成功打开时候,直接返回报错
        return -EBADF;  
    }  
  
    mOut.writeInt32(BC_ENTER_LOOPER);  //写入Binder调用命令
    flushCommands();  
    *fd = mProcess->mDriverFD;  //保存Binder驱动的文件描述符到fd当中
    return 0;  
}

void IPCThreadState::flushCommands()  
{  
    if (mProcess->mDriverFD < 0)  
        return;  
    talkWithDriver(false);  //与驱动进行交互
    if (mOut.dataSize() > 0) {  //二次确认,未成功则继续交互
        talkWithDriver(false);  
    }   
}

可以看到上面的代码主要是与Binder驱动交互,并且执行命令进入Binder的循环,且拿到binder驱动的文件描述符,其中talkWithDriver为与binder交互的具体流程,后续在介绍其代码。

拿到binder驱动的文件描述符后执行Looper的addFd方法,最终执行的方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//Looper.cpp
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {  
	...
  
    { // 此代码段上锁
        AutoMutex _l(mLock);  
    
        if (mNextRequestSeq == WAKE_EVENT_FD_SEQ) mNextRequestSeq++;  
        const SequenceNumber seq = mNextRequestSeq++;  
  
        Request request;  
        request.fd = fd;  
        request.ident = ident;  
        request.events = events;  
        request.callback = callback;  
        request.data = data;  
  
        epoll_event eventItem = createEpollEvent(request.getEpollEvents(), seq);  
        auto seq_it = mSequenceNumberByFd.find(fd);  
        if (seq_it == mSequenceNumberByFd.end()) {  //列表中不存在该fd
            int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);  
            if (epollResult < 0) {  
                ... 
                return -1;  
            }  
            mRequests.emplace(seq, request);  
            mSequenceNumberByFd.emplace(fd, seq);  
        } else {  
            int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_MOD, fd, &eventItem);  
            if (epollResult < 0) {  
                ...
                return -1;
            }  
            const SequenceNumber oldSeq = seq_it->second;  
            mRequests.erase(oldSeq);  
            mRequests.emplace(seq, request);  
            seq_it->second = seq;  
        }  
    } // release lock  
    return 1;  
}

上面的代码使用我们的fd创建了epoll_event,并且调用系统调用epoll_ctl来进行注册,只是代码中判断了fd在不再mSequenceNumberByFd当中,在的话使用的是EPOLL_CTL_ADD,不在则使用EPOLL_CTL_MOD。 这里我们需要了解一下epoll,它是linux中的一种高效、可扩展的I/O时间通知机制,而我们这里做的就是监听Binder驱动的FD,当Binder驱动中有变化通知到epoll的文件描述符,也就是我们这里的Looper的回调就可以收到,具体监听的事件为Looper::EVENT_INPUT,从而也就会执行BinderCallbackhandlePolledCommands方法,这个我们留到后面再分析。

看到这,我们可以再看一下main方法的第16行代码,调用的ClientCallbackCallback.setupTo方法,这个类也同样是一个LooperCallback的子类,其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static sp<ClientCallbackCallback> setupTo(const sp<Looper>& looper, const sp<ServiceManager>& manager) {  
    sp<ClientCallbackCallback> cb = sp<ClientCallbackCallback>::make(manager);  
  
    int fdTimer = timerfd_create(CLOCK_MONOTONIC, 0 /*flags*/);  
    
    itimerspec timespec {  
        .it_interval = {  
            .tv_sec = 5,  
            .tv_nsec = 0,  
        },  
        .it_value = {  
            .tv_sec = 5,  
            .tv_nsec = 0,  
        },  
    };  
  
    int timeRes = timerfd_settime(fdTimer, 0 /*flags*/, &timespec, nullptr);  

    int addRes = looper->addFd(fdTimer,  
                               Looper::POLL_CALLBACK,  
                               Looper::EVENT_INPUT,  
                               cb,  
                               nullptr);  
  
    return cb;  
}

这里addFd的代码和之前的一样,但是前面这个timerfd令人感到疑惑,查询一番之后才知道,这里是linux中的定时机制,timerfd为一个基于文件描述符的定时接口,我们这里的代码则是每个5秒钟触发一次,也就是说每隔5秒就会执行一次这个对象的handleEvent方法。

当有客户端请求添加Service或者查询Service等操作的时候,BinderCallbak的handlePolledCommands就会执行去处理,内部会调用如下代码:

1
IPCThreadState::self()->handlePolledCommands();

上面的代码就是会去读取binder传过来的数据,进行处理,具体内容留到后面再分析。

看完评论一下吧

Android源码分析:Binder概述与ServiceManager获取

2024-09-04 19:09:48

阅读Android系统源码,Binder是绕不过去的东西,前面看ContentProvider,Activity都有Binder的身影,因此决定还是先把Binder的部分看一看。本文主要简单介绍一下Binder的历史和它的基本架构,介绍Binder的ServiceManager我们在使用的时候如何去拿到它,同时推荐一些Binder的学习资料。

Binder简介

对于普通的Android的应用开发来说,进程的概念是被弱化的。这得益于系统已经帮助我们把Activity,ContentProvider,Broadcast,Service等涉及到跨进程的组件做了很好的封装。我们知道Android也是基于Linux进行开发的,那比如存在跨进程,也就必然存在跨进程通讯。Linux当中跨进程通讯常常使用共享内存、信号量、管道等方式,不过Android中为了安全和使用的便利性,则大部分地方都是使用了Binder。

Binder并不是新提出来的一套跨进程通信机制,它是基于OpenBinder实现的。Binder最早是Be公司开发的, George Hoffman需要一种机制让Be的互联网设备的Javascript UI层与地秤系统服务发生交互,边开发了Binder。后来Be公司的工程师加入了PalmSource开发Palm OS,再后来加入Android,Binder也一直被他们采用,并且也在一直演化,对这段历史感兴趣的话,可以看看《安卓传奇:Android缔造团队回忆录》这本书。开源的OpenBinder是可以工作在Linux内核中的,在2015年已经被合并到Linux 内核3.19版本当中,不仅仅Android,华为的鸿蒙系统当中也在使用Binder。

Binder基本架构

Android中的Binder包括Binder驱动,ServiceManager,Binder服务,他们的关系大概如上图所示。Binder驱动位于Linux内核层,它主要用于实现多个进程之间的通信。ServiceManager位于Android Framework层,用于Binder服务的注册和查找,相当于网络服务中的DNS。而Binder服务的Server端和Client端就是典型的C/S架构,它们通过Binder驱动来进行交互。Android中有两种Binder服务,一种是类似于AMS,PMS这种的系统服务,它们是有名称的服务,注册在ServiceManager当中,Client可以通过名称查找到他们进行使用。还存在另一种匿名Binder服务,比如我们自己通过AIDL创建的,这种我们会直接通过其他的Binder服务把Binder引用传递到客户端,从而双方可以进行通讯。

Binder驱动的源码是在Linux当中的,暂时先不关注了。我这里主要会去看Android Framework层当中Binder相关的代码。ServiceManager本身也是一个Binder,它的ID为0,因此可以很简单的拿到,它的初始化我就不关注了。首先会去关注我们在应用层如何去拿到ServiceManager,因为只有拿到它才能够使用它去注册Binder和获取Binder。其实我们再去看看ServiceManger的addService如何注册一个Binder服务,以及getService 如何获取一个Binder服务。这些看完之后,我们也就知道了Binder的完整运行过程,因为addServicegetService本身也是binder调用,其中我们也会分析Framwork调用kernel相关的代码。最后我们再看看匿名Binder,AIDL,这样差不多就可以对于Framework层的Binder有了全面的了解。

获取ServiceManager

ServiceManager本身是存在一个单独的进程的,并且是在系统启动的时候就启动了它。而我们在其他进程想要通过它来注册服务或者获取服务,就需要首先拿到它的Ibinder对象。通常会用如下的方式获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private static IServiceManager getIServiceManager() {  
    if (sServiceManager != null) {  
        return sServiceManager;  
    }  
  
    // Find the service manager  
    sServiceManager = ServiceManagerNative  
            .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));  
    return sServiceManager;  
}

上面的代码首先会拿本地的缓存,拿不到才会真正调用获取ServiceManager的步骤,我们先看看这个BinderInternal.getContextObject()方法,它是一个native方法,它的实现在base/core/jni/android_util_Binder.cpp当中,代码如下:

1
2
3
4
5
static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)  
{  
    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);  
    return javaObjectForIBinder(env, b);  
}

首先调用ProcessState::self()来拿到ProcessState实例,它内部会执行如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sp<ProcessState> ProcessState::init(const char *driver, bool requireDefault)  
{  
  
    if (driver == nullptr) {  
        std::lock_guard<std::mutex> l(gProcessMutex);  
        if (gProcess) {  
            verifyNotForked(gProcess->mForked);  
        }  
        return gProcess;  
    }  
  
    [[clang::no_destroy]] static std::once_flag gProcessOnce;  
    std::call_once(gProcessOnce, [&](){  
        if (access(driver, R_OK) == -1) {  
            driver = "/dev/binder";  
        }  
  
        int ret = pthread_atfork(ProcessState::onFork, ProcessState::parentPostFork,  
                                 ProcessState::childPostFork);  //注册fork进程的监听
        LOG_ALWAYS_FATAL_IF(ret != 0, "pthread_atfork error %s", strerror(ret));  
  
        std::lock_guard<std::mutex> l(gProcessMutex);  
        gProcess = sp<ProcessState>::make(driver);  //智能指针初始化
    });  
  
    if (requireDefault) {
	    ... 
    }  
  
    verifyNotForked(gProcess->mForked);  
    return gProcess;  
}

上面的代码传入的参数driver值为/dev/binder也就是binder驱动的地址,requireDefault为false。上面的代码中的std:call_once方法为android的libc所提供,就是保证下面的代码段只会执行一次,这个实现也就是为了实现单例,和Java代码中的其实是差不多的。代码中的make方法内部实际会调用ProcessState的构造方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ProcessState::ProcessState(const char* driver)  
      : mDriverName(String8(driver)),  
        mDriverFD(-1),  
        mVMStart(MAP_FAILED),  
        mThreadCountLock(PTHREAD_MUTEX_INITIALIZER),  
        mThreadCountDecrement(PTHREAD_COND_INITIALIZER),  
        mExecutingThreadsCount(0),  
        mWaitingForThreads(0),  
        mMaxThreads(DEFAULT_MAX_BINDER_THREADS),  
        mStarvationStartTimeMs(0),  
        mForked(false),  
        mThreadPoolStarted(false),  
        mThreadPoolSeq(1),  
        mCallRestriction(CallRestriction::NONE) {  
    base::Result<int> opened = open_driver(driver);  
  
    if (opened.ok()) {  
        mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, opened.value(), 0);  
        if (mVMStart == MAP_FAILED) {  
            close(opened.value());    //mmap失败,关闭binder文件描述符
            opened = base::Error()  
                    << "Using " << driver << " failed: unable to mmap transaction memory.";  
            mDriverName.clear();  
        }  
    }  
    verifyNotForked(gProcess->mForked); //检查当前的实例不是fork之后的只进程的实例否则报错
    if (opened.ok()) {  
        mDriverFD = opened.value();  //记录binder的文件描述符
    }  
}

open_driver内部就是调用linux的系统调用open打开binder驱动,并通过ioctl获取驱动打开状态以及进行驱动的一些设置如最大线程数等,这里就查看相关代码了。

打开驱动后又会调用mmap把进行内存映射并保存内存指针到mVMStart上,其中内存映射的大小为BINDER_VM_SIZE,定义如下:

1
2
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
#define _SC_PAGE_SIZE           0x0028

其中sysconf(_SC_PAGE_SIZE)之前的值为4k,最新的Android 15改成了16K,那我们这里仍然以来版本计算,可以得到值为1016Kb,这也就是我们使用Binder交互时候数据传输的限制。

这里我们拿到了binder的文件描述符,也完成了内存映射,也就完成了ProcessState的初始化。ProcessState这个对象如它的名字,在每个进程当中只会有一个实例。

有了实例我们又可以继续看getContextObject,主要代码如下:

1
2
3
4
5
6
7
8
9
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)  
{  
    sp<IBinder> context = getStrongProxyForHandle(0);  
  
    if (context) {  
          internal::Stability::markCompilationUnit(context.get());  //更新Binder的Stability,展示可以跳过
    } 
    return context;  
}

上面主要关注getStrongProxyForHandle(0),这里传入的id为0,也就是专属于ServiceManager的,此方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
ProcessState::handle_entry* ProcessState::lookupHandleLocked(int32_t handle)  
{  
    const size_t N=mHandleToObject.size();  
    if (N <= (size_t)handle) {  
        handle_entry e;  
        e.binder = nullptr;  
        e.refs = nullptr;  
        status_t err = mHandleToObject.insertAt(e, N, handle+1-N);  
        if (err < NO_ERROR) return nullptr;  
    }  
    return &mHandleToObject.editItemAt(handle);  
}


sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)  
{  
    sp<IBinder> result;  
  
    AutoMutex _l(mLock);  
  
    handle_entry* e = lookupHandleLocked(handle);  
  
    if (e != nullptr) {  
        IBinder* b = e->binder;  
        if (b == nullptr || !e->refs->attemptIncWeak(this)) {  
            if (handle == 0) {  
                IPCThreadState* ipc = IPCThreadState::self();  
                CallRestriction originalCallRestriction = ipc->getCallRestriction();  //获取当前的调用限制
                ipc->setCallRestriction(CallRestriction::NONE);  //设置限制为空
  
                Parcel data;  
                status_t status = ipc->transact(  
                        0, IBinder::PING_TRANSACTION, data, nullptr, 0); //调用ping,获取当前Binder的状态 
  
                ipc->setCallRestriction(originalCallRestriction);  //恢复原先的限制
  
                if (status == DEAD_OBJECT)  
                   return nullptr;  
            }  
  
            sp<BpBinder> b = BpBinder::PrivateAccessor::create(handle);  
            e->binder = b.get();  
            if (b) e->refs = b->getWeakRefs();  
            result = b;  
        } else {  
	        result.force_set(b);  
	        e->refs->decWeak(this);  
        }  
    }  
  
    return result;  
}


struct handle_entry {  
    IBinder* binder;  
    RefBase::weakref_type* refs;  
};

Vector<handle_entry> mHandleToObject;

handle_entry为结构提,其中存放了IBinderrefs,refs为一个弱引用,用于记录Binder的使用数量,这些entry有存放在动态数组mHandleToObject当中。 查找过程很简单,就是数组中有则返回,无则插入一条。对于ServiceManager,此处会调用IPCThreadState的相关方法,首先看看它的self方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
IPCThreadState* IPCThreadState::self()  
{  
    if (gHaveTLS.load(std::memory_order_acquire)) {  
restart:  
        const pthread_key_t k = gTLS;  
        IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);  
        if (st) return st;  
        return new IPCThreadState;  
    }  
  
    // Racey, heuristic test for simultaneous shutdown.  
    if (gShutdown.load(std::memory_order_relaxed)) {  
        ALOGW("Calling IPCThreadState::self() during shutdown is dangerous, expect a crash.\n");  
        return nullptr;  
    }  
  
    pthread_mutex_lock(&gTLSMutex);  
    if (!gHaveTLS.load(std::memory_order_relaxed)) {  
        int key_create_value = pthread_key_create(&gTLS, threadDestructor);  
        if (key_create_value != 0) {  
            pthread_mutex_unlock(&gTLSMutex);  
            ALOGW("IPCThreadState::self() unable to create TLS key, expect a crash: %s\n",  
                    strerror(key_create_value));  
            return nullptr;  
        }  
        gHaveTLS.store(true, std::memory_order_release);  
    }  
    pthread_mutex_unlock(&gTLSMutex);  
    goto restart;  
}

这里的gHaveTLS类型为atomic<bool>和java中的AtomicBoolean一样都是原子类型安全的Boolean,这里的TLS不是https中我们说的那个TLS而是表示Thread Local Storage,这里我们就可以明白,此处我们是把IPCThreadState存放在Thread Local中,从而保证每一个线程拥有一个IPCThreadState对象,这个类的构造函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IPCThreadState::IPCThreadState()  
      : mProcess(ProcessState::self()),  
        mServingStackPointer(nullptr),  
        mServingStackPointerGuard(nullptr),  
        mWorkSource(kUnsetWorkSource),  
        mPropagateWorkSource(false),  
        mIsLooper(false),  
        mIsFlushing(false),  
        mStrictModePolicy(0),  
        mLastTransactionBinderFlags(0),  
        mCallRestriction(mProcess->mCallRestriction) {  
    pthread_setspecific(gTLS, this);  //key 为gTLS, value为IPCThreadState,存到ThreadLocal中。
    clearCaller();  
    mIn.setDataCapacity(256);  
    mOut.setDataCapacity(256);  
}

void IPCThreadState::clearCaller()  
{  
    mCallingPid = getpid();  
    mCallingSid = nullptr;  // expensive to lookup  
    mCallingUid = getuid();  
}

构造方法中除了设置mInmOut这两个Parcel外,就是设置Callinguidpid为当前调用进程的值。

回到getStrongProxyForHandle方法,检查binder状态的代码看我的注释就好,可以继续看第41行,它内部调用了如下代码:

1
static sp<BpBinder> create(int32_t handle) { return BpBinder::create(handle); }

而这个create方法内部也主要调用了如下代码:

1
return sp<BpBinder>::make(BinderHandle{handle}, trackedUid);

这里使用了强引用指针,我们解析以下实际上是调用了如下代码:

1
BpBinder(BinderHandle{handle}, trackedUid);

也就是创建了一个BpBinder,在它的构造方法中会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
IPCThreadState::self()->incWeakHandle(this->binderHandle(), this);



void IPCThreadState::incWeakHandle(int32_t handle, BpBinder *proxy)  
{  
    LOG_REMOTEREFS("IPCThreadState::incWeakHandle(%d)\n", handle);  
    mOut.writeInt32(BC_INCREFS);  
    mOut.writeInt32(handle);  
    if (!flushIfNeeded()) {  
        // Create a temp reference until the driver has handled this command.  
        proxy->getWeakRefs()->incWeak(mProcess.get());  
        mPostWriteWeakDerefs.push(proxy->getWeakRefs());  
    }  
}

上面写入mOut的数据,在将来调用flushCommands的时候会与Binder驱动交互,这个后面再分析。到这里我们就拿到ServiceManager的BpBinder对象了,但是我们现在还是在native层,因此还需要把对象返回到java层,我们这个时候可以看javaObjectForIBinder方法的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)  
{  
    if (val->checkSubclass(&gBinderOffsets)) {  
        // It's a JavaBBinder created by ibinderForJavaObject. Already has Java object.  
        jobject object = static_cast<JavaBBinder*>(val.get())->object();  
        return object;  
    }  
  
    BinderProxyNativeData* nativeData = new BinderProxyNativeData();  
    nativeData->mOrgue = new DeathRecipientList;  
    nativeData->mObject = val;  
  
    jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,  
            gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());  
    if (env->ExceptionCheck()) {  
        // In the exception case, getInstance still took ownership of nativeData.  
        return NULL;  
    }  
    BinderProxyNativeData* actualNativeData = getBPNativeData(env, object);  
    if (actualNativeData == nativeData) {  
        // Created a new Proxy  
        uint32_t numProxies = gNumProxies.fetch_add(1, std::memory_order_relaxed);  
        uint32_t numLastWarned = gProxiesWarned.load(std::memory_order_relaxed);  
        ....  
    } else {  
        delete nativeData;  
    }  
  
    return object;  
}

上面的代码首先去看看我们现在的指针中的类是否为Java层Binder类的子类,这种情况在binder由ibinderForJavaObject创建,我们这里不是。因此会使用下面的代码,这里gBinderProxyOffsets的相关值如下:

1
2
3
4
5
6
7
8
9
const char* const kBinderProxyPathName = "android/os/BinderProxy";
jclass clazz = FindClassOrDie(env, kBinderProxyPathName);  
gBinderProxyOffsets.mClass = MakeGlobalRefOrDie(env, clazz);  
gBinderProxyOffsets.mGetInstance = GetStaticMethodIDOrDie(env, clazz, "getInstance",  
        "(JJ)Landroid/os/BinderProxy;");  
gBinderProxyOffsets.mSendDeathNotice =  
        GetStaticMethodIDOrDie(env, clazz, "sendDeathNotice",  
                               "(Landroid/os/IBinder$DeathRecipient;Landroid/os/IBinder;)V");  
gBinderProxyOffsets.mNativeData = GetFieldIDOrDie(env, clazz, "mNativeData", "J");

也就是说这里会调用BinderProxygetInstance方法来创建BinderProxy实例,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private static BinderProxy getInstance(long nativeData, long iBinder) {  
    BinderProxy result;  
    synchronized (sProxyMap) {  
        try {  
            result = sProxyMap.get(iBinder);  
            if (result != null) {  
                return result;  
            }  
            result = new BinderProxy(nativeData);  
        } catch (Throwable e) {  
NativeAllocationRegistry.applyFreeFunction(NoImagePreloadHolder.sNativeFinalizer,  nativeData);  
            throw e;  
        }  
        NoImagePreloadHolder.sRegistry.registerNativeAllocation(result, nativeData);  
        sProxyMap.set(iBinder, result);  
    }  
    return result;  
}

主要就是把nativeData放到BinderProxy对象当中,并且用iBinder做为key放到缓存map当中去。到这里native层的代码就全部分析完了。可以继续回到java层的代码。接下来就是调用ServiceManagerNative.asInterface方法,代码如下:

1
2
3
4
5
6
public static IServiceManager asInterface(IBinder obj) {  
    if (obj == null) {  
        return null;  
    }  
    return new ServiceManagerProxy(obj);  
}

其中就是用ServiceManagerProxy对我们刚刚拿到的BinderProxy进行代理。这样便完成了获取ServiceManager的整个流程。

获取ServiceManager的时序图如下所示:

sequenceDiagram
	ServiceManager->>BinderInternal: getContextObject()
	BinderInternal->>ProcessState: self()
	ProcessState->>ProcessState: getContextObject()
	ProcessState->>ProcessState: getStrongProxyForHandle()
	ProcessState->>IPCThreadState: self()
	IPCThreadState->>IPCThreadState: transact
	note over IPCThreadState: PING_TRANSACTION
	ProcessState-->>BinderInternal: createBinder
	note right of BinderInternal: BpBinder
	BinderInternal-->>ServiceManager: javaObjectForIBinder
	note right of ServiceManager: BinderProxy
	ServiceManager-->>ServiceManager: asInterface
	note right of ServiceManager: ServiceManagerProxy

我们现在所分析的流程,本质上还是客户端去获取一个Binder的流程,当然这个binder比较特殊,它直接写死了id为0。可以再回顾一下刚刚涉及到的类。 首先是ProcessState,每个进程都会有一个它的实例,它用于维护打开binder驱动的文件描述符、维护binder线程池以及创建IPCThreadState等。IPCThreadState则用于具体的Binder连接,它会通过ThreadLocal依附于线程,与Binder驱动交互的相关代码都在它的内部。客户端在native端的binder对象为BpBinder,在java端的对象则为BinderProxy。

Binder学习资料

我的文章只会介绍Android Framework层Binder相关的知识,Binder驱动是在Kernel当中的,我不会涉及。另外ServiceManager也是一个系统的守护进程,系统启动的时候也就会启动,我可能也不会分析了。因此推荐以下资料,方便大家在学习Binder,同时对于我没有涉及到的部分也可以参考。

  1. Binder学习指南 这个介绍的还比较通俗易懂
  2. Gityuan Binder系列详解基于Android 6.0,内容详细,从驱动到应用层全部都有讲解
  3. Android深入浅出之Binder机制 邓平凡老师的讲解,可以大概弄清楚binder的机制。

到这里分析完这个流程,我们后面就可以分析addService流程了,待到下次文章继续分享。相互交流才能更好的提高,欢迎读者朋友评论交流。

看完评论一下吧

八月月报-折腾不停歇

2024-09-02 13:39:58

8月将结束,秋天已经到来,酷暑还未结束。把小朋友送到学校,才有时间又来回顾一个月。如题所示,博客折腾了许多,也体验了一下黑神话,详细见下文。

黑神话:悟空

悟空上线,全网热议,每个群里都有人在讨论,似乎身边的每个人都在玩。对于这种现象级的国产三A大作,肯定要支持一下。然而,却没时间玩,这十来天也只玩了一小会,牯护院打过去了,广智打了十几把还没过,就暂时搁置下来了。之前电脑上的游戏主要是玩回合制的比较多,这个类型的游戏之前很少,种种原因,觉得难度还挺大。 游戏画面精美,里面的碑文都能够清晰可见汉字,值得夸赞。各种建筑据说是按照很多中国的古建筑建模,即使是我这种游戏手残党也值得进入游戏游山玩水。

玩水

这个月带着娃出去完了两趟,一次是莫干山玩水,浙江多山水,适宜夏季游玩,高温天泡在水里清凉许多,详见之前博文

后又带着小孩跑到金山城市沙滩玩了一天水。所周知,上海的海边都是滩涂,金山城市沙滩和奉闲碧海金沙都是人造的,水域也都是人工围起来过滤的。虽然比不上山东三亚的沙滩,但是小朋友仍然热情满满,玩了一天到了离开仍然依依不舍。

折腾

博客自从21年用Hugo重新搭建之后,一直没怎么修改过。这个月有空就想着改改,主题修改了,还加了个足迹地图,更新了自我介绍,支持mermaid图表渲染支持,以及其他的一些小功能,详见之前的文章。不过改版的风格仍然还是简洁风,博客还是以内容为主。 另外博客域名原先是在godaddy注册的,DNS又用了阿里云的服务,而域名即将到期,续费时候发现居然一年价格差不多要200多块人民币,而比较了一下Cloudflare一年只要100多,可以省差不多100块,于是便把域名转移到了Cloudflare。迁移还比较简单,把隐私DNS关掉,从Godaddy拿到转出Code,到Cloudflare接收,并付掉一年的费用即可。 一不做二不休,索性将域名的DNS也迁移到了Cloudflare,用上了Cloudflare的代理功能,不知道最近大家访问博客是否有变更快呢。在迁移过程中踩了个坑,对于使用了Cloudflare代理的网站,默认的SSL加密模式为Flexible(灵活),也就是用户用户到Cloudflare为https,而Cloudflare到源站为http。对于源站本来已经是https的情况下这样访问就会报错,这个时候有两种做法,可以把源站的https关掉,或者使用Cloudflare的full模式(完全)。为了更加安全我是用了Full模式。我使用的Vercel提供的静态站点服务,对于可以自己设置证书的,也可以使用Cloudflare的Full (strict)模式,也就是严格安全,源站使用Cloudflare签发的证书,Cloudflare会验证源站点证书,这个会更加安全。 除了博客之外,还把自己家里的内网服务重新折腾了一下。首先是搭建对外访问的VPN服务,实现随时随地的无感翻墙回家,当然最后试了几种方案都失败了,就不详术了。另外就是借助Lucky,给对外暴露的内网服务实现了SSL证书自动签名续签,以及在家中OpenWrt搭建了vaultwarden密码管理服务,已运行十余天,目前没啥问题,详见博文

Android开发乃是老本行,最近工作不太忙,想起来把系统源码拿出来读一读,希望对于系统能够有更加深入的了解。 目前进度读完Activity启动,ContentProvider代码,应用进程启动过程。对于系统代码来说,已读的部分也只是冰山一角,后面几个月仍然需要继续努力。目前读完的部分,还只写了三四篇博文,其他部分或有草草的笔记,或是缺少流程图类图,还无法成文,仍需继续等待整理。 写成文章有几个好处,首先成文不同与笔记,需多次整理自己的行文,有助于发现自己的疏漏。另外因为写给别人看,更加需要时序图,流程图之类的图形来便于理解,在画图的过程中也加深了自己对相关代码的理解。最后,写成文章发布到网络,也便于与网友进行交流互相学习。因此后续看代码,仍有必要继续写成文章发布到博客中来。

这个月在看一部电视剧«唐朝诡事录»,看完了第一部,又在看第二部。大部头的电视剧看着挺累的,比较耗费时间,不过这个剧的特点是几集就是一个案子所以又不是太累。剧情总体还不错,有推理有细节。槽点嘛,就是太子和公主的权力争夺太假太扯淡了。

电影看了个«云边有个小卖部»,剧情给我的感觉是伪奋斗,伪喜剧,要不是家人要看差点就半途不看了。不过这个村里风景很美,种草了宁波奉化那边的山村,想去看看。

另外还看了个电影《朝云暮雨》,范伟和周冬雨饰演两个从监狱里面出来的人,两个人相互救赎,最后以一个成为植物人,范伟来照顾周冬雨结束。据说是根据真实故事改编,剧中的结局在我看来是一种悲剧结尾,但是在主人公来说可能又是一种好的结局。这个剧的故事还不错,值得推荐一看。这部剧在黄山黟县拍摄,风景也很棒,值得一去。

因为总的时间是有限的,看剧花费了很多时间,看书的时间就少了很多。这个月只看了两本书,一本是«西藏西藏»,虽然还只看了一半,但是对西藏已是神往。作者介绍了他在西藏各处的经历以及所见美景,搭配着相关照片,带我们领略西藏的人文。

另一本是逛书店意外所得,漫画版本的曼昆《经济学原理》,因为是漫画,读起来更容易,又配有案例,生动易于理解,仅几个小时就把第一册读完,遂激情下单,把七本全买了回来慢慢看。如果有读者对于经济学有兴趣,不妨也可以找找这个书来看看。

尾声

八月还算充实,这样挺好。最近一次吃饭,听人提到人生由四个部分组成,过去,未来,成就,意图,觉得值得思考。我们可以通过未来做的事来实现我们的意图,对于已经过了而立之年的我们,当应该想想自己的人生目标(意图),以便走到人生终点回望来路不留遗憾。

看完评论一下吧

Android源码分析: ContentProvider查询以及数据变化监听分析

2024-08-29 18:51:12

之前已经分析了启动应用安装ContentProvider,使用时获取ContentProvider,我们这里再分析一下使用ContentProvider查询数据已经监听ContentProvider数据变化的情况。

查询数据

上次的文章已经介绍了使用query的方法,并且已经介绍完了通过acquireProvider获取到ContentProvider,如果是是本地应用的话拿到的是Transport对象,如果是查询其他应用(不严谨的说法,其他应用也要排查userId不同,且不共享签名),则拿到的是ContentProviderProxy,这里我们要分析的查询是其他应用的情况,因此我们需要关注ContentProviderProxyquery方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override  
public Cursor query(@NonNull AttributionSource attributionSource, Uri url,  
        @Nullable String[] projection, @Nullable Bundle queryArgs,  
        @Nullable ICancellationSignal cancellationSignal)  
        throws RemoteException {  
    BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor();  
    Parcel data = Parcel.obtain();  
    Parcel reply = Parcel.obtain();  
    try {  
        data.writeInterfaceToken(IContentProvider.descriptor);  
  
        attributionSource.writeToParcel(data, 0);  
        url.writeToParcel(data, 0);  
        int length = 0;  
        if (projection != null) {  
            length = projection.length;  
        }  
        data.writeInt(length);  
        for (int i = 0; i < length; i++) {  
            data.writeString(projection[i]);  
        }  
        data.writeBundle(queryArgs);  
        data.writeStrongBinder(adaptor.getObserver().asBinder());  
        data.writeStrongBinder(  
                cancellationSignal != null ? cancellationSignal.asBinder() : null);  
  
        mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);  
  
        DatabaseUtils.readExceptionFromParcel(reply);  
  
        if (reply.readInt() != 0) {  
            BulkCursorDescriptor d = BulkCursorDescriptor.CREATOR.createFromParcel(reply);  
            Binder.copyAllowBlocking(mRemote, (d.cursor != null) ? d.cursor.asBinder() : null);  
            adaptor.initialize(d);  
        } else {  
            adaptor.close();  
            adaptor = null;  
        }  
        return adaptor;  
    } catch (RemoteException ex) {  
        adaptor.close();  
        throw ex;  
    } catch (RuntimeException ex) {  
        adaptor.close();  
        throw ex;  
    } finally {  
        data.recycle();  
        reply.recycle();  
    }  
}

这个代码比较简单,把需要查询的条件写入到Parcel中,然后通过mRemote进行binder调用,在reply中拿到远端执行的结果。如果执行成功了,则通过BulkCursorDescriptor来读取reply中的数据,主要是拿到了其中IBulkCursor的Binder对象和CursorWindow这个对象。在查询的流程中会涉及到很多的类,我这里画了使用端和服务端会使用到的Cursor所涉及到相关类和接口。

其中BulkCursorToCursorAdapter为客户端使用,用于读取服务端通过binder传过来的数据,其中的封装和使用,我们后面还会继续看到。关于服务端的我们先继续往后看代码,随后会涉及到相关的类。

可以看看Provider服务端是如何把这些东西放到reply中的。我们这个时候可以看一下ContentProviderNativeonTransactQUERY_TRANSACTION的这一分支:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
data.enforceInterface(IContentProvider.descriptor);  
  
AttributionSource attributionSource = AttributionSource.CREATOR  
        .createFromParcel(data);  
Uri url = Uri.CREATOR.createFromParcel(data);  
  
// String[] projection  
int num = data.readInt();  
String[] projection = null;  
if (num > 0) {  
    projection = new String[num];  
    for (int i = 0; i < num; i++) {  
        projection[i] = data.readString();  
    }  
}  
  
Bundle queryArgs = data.readBundle();  
IContentObserver observer = IContentObserver.Stub.asInterface(  
        data.readStrongBinder());  
ICancellationSignal cancellationSignal = ICancellationSignal.Stub.asInterface(  
        data.readStrongBinder());  

这是其中的第一部分代码,就是把binder传过来的查询需要的数据进行反序列化。

1
2
Cursor cursor = query(attributionSource, url, projection, queryArgs,  
        cancellationSignal);

第二部分为调用query进行查询,我们知道在数据提供端,其实是Transport,可以看看它的query方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override  
public Cursor query(@NonNull AttributionSource attributionSource, Uri uri,  
        @Nullable String[] projection, @Nullable Bundle queryArgs,  
        @Nullable ICancellationSignal cancellationSignal) {  
    uri = validateIncomingUri(uri);  
    uri = maybeGetUriWithoutUserId(uri);  
    if (enforceReadPermission(attributionSource, uri)  
            != PermissionChecker.PERMISSION_GRANTED) {  
        if (projection != null) {  
            return new MatrixCursor(projection, 0);  
        }  
  
       Cursor cursor;  
        final AttributionSource original = setCallingAttributionSource(  
                attributionSource);  
        try {  
            cursor = mInterface.query(  
                    uri, projection, queryArgs,  
                    CancellationSignal.fromTransport(cancellationSignal));  
        } catch (RemoteException e) {  
            throw e.rethrowAsRuntimeException();  
        } finally {  
            setCallingAttributionSource(original);  
        }  
        if (cursor == null) {  
            return null;  
        }  
  
        // Return an empty cursor for all columns.  
        return new MatrixCursor(cursor.getColumnNames(), 0);  
    }  
    traceBegin(TRACE_TAG_DATABASE, "query: ", uri.getAuthority());  
    final AttributionSource original = setCallingAttributionSource(  
            attributionSource);  
    try {  
        return mInterface.query(  
                uri, projection, queryArgs,  
                CancellationSignal.fromTransport(cancellationSignal));  
    } catch (RemoteException e) {  
        throw e.rethrowAsRuntimeException();  
    } finally {  
        setCallingAttributionSource(original);  
        Trace.traceEnd(TRACE_TAG_DATABASE);  
    }  
}

其中的代码也是比较简单的,首先调用enforceReadPermission检查是否有使用这个ContentProvider的权限,如果有权限则调用mInterface.query,我们看源码就知道,这个mInterface也就是一个ContentProvider,也就是我们开发过程实现的那个ContentProvider,query方法也就是我们自己的实现。

我们先不着急分析后面的代码,我们前面说过如果是本进程的ContentProvider查询会直接调用Transportquery方法,那么就不存在binder调用,而是直接调用了我们所实现的query。这个实现还是很妙的,值得我们学习。对于我们跨进程的调用,还需要看ContentProviderNativeonTransact后面的代码,也就是我们要说的第三部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (cursor != null) {  
    CursorToBulkCursorAdaptor adaptor = null;  
  
    try {  
        adaptor = new CursorToBulkCursorAdaptor(cursor, observer,  
                getProviderName());  
        cursor = null;  
  
        BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor();  
        adaptor = null;  
  
        reply.writeNoException();  
        reply.writeInt(1);  
        d.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);  
    } finally {  
        // Close cursor if an exception was thrown while constructing the adaptor.  
        if (adaptor != null) {  
            adaptor.close();  
        }  
        if (cursor != null) {  
            cursor.close();  
        }  
    }  
} else {  
    reply.writeNoException();  
    reply.writeInt(0);  
}

这里在拿到数据的时候,通过CursorToBulkCursorAdapter把刚刚查询到Cursor进行了包装,并且通过BulkCursorDescriptor写入到reply中。这样我们刚刚调用端就可以拿到了。 我们看CursorToBulkCursorAdapter可以看到,它的内部又用CrossProcessCursorWrapper来对Cursor进行了封装。

我们已经完成查询,并且获取到Cursor的封装,接下来我们就可以看一下数据的读取了。通过我们前面的Cursor的各个相关类的关系图,我们知道在客户端我们所拿到的是BulkCursorToCursorAdapter,它的初始化代码如下:

1
2
3
4
5
6
7
8
9
public void initialize(BulkCursorDescriptor d) {  
    mBulkCursor = d.cursor;  
    mColumns = d.columnNames;  
    mWantsAllOnMoveCalls = d.wantsAllOnMoveCalls;  
    mCount = d.count;  
    if (d.window != null) {  
        setWindow(d.window);  
    }  
}

以下为服务端和客户端交互的流程时序图,通过这个图我们可以具体看到查询过程服务端和客户端交互,以及两边封装的类:

sequenceDiagram
App->>ContentProviderProxy: query
ContentProviderProxy->>+Transport(Server): Binder(QUERY_TRANSACTION)
Transport(Server)->>+ContentProvider: query
ContentProvider -->>- Transport(Server): return query result
note right of Transport(Server): Cursor
Transport(Server) -->>- ContentProviderProxy: Binder(write reply)
note left of Transport(Server): CursorToBulkCursorAdaptor
ContentProviderProxy --> App: return query result
note left of ContentProviderProxy: BulkCursorToCursorAdaptor

服务端传输数据到调用端

可以知道我们主要从服务端拿到两个东西,一个是BulkCursor它是后面我们的数据移动的Binder操作类,CursorWindow则用来存放当前位置的数据。当我们调用CursormoveToNext的时候,就会调用BulkCursorToCursorAdapteronMove方法,进而又会通过binder调用CursorToBulkCursorAdapteronMove方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override  
public boolean onMove(int oldPosition, int newPosition) {  
    throwIfCursorIsClosed();  
  
    try {  
        if (mWindow == null  
                || newPosition < mWindow.getStartPosition()  
                || newPosition >= mWindow.getStartPosition() + mWindow.getNumRows()) {  
            setWindow(mBulkCursor.getWindow(newPosition));  
        } else if (mWantsAllOnMoveCalls) {  
            mBulkCursor.onMove(newPosition);  
        }  
    } catch (RemoteException ex) {   
        return false;  
    }  
  
    if (mWindow == null) {  
        return false;  
    }  
  
    return true;  
}

当window还没有初始化的时候,会调用setWindowsetWindow很简单,但是mBulkCursor.getWindow却不简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override  
public CursorWindow getWindow(int position) {  
    synchronized (mLock) {  
        throwIfCursorIsClosed();  
  
        if (!mCursor.moveToPosition(position)) {  
            closeFilledWindowLocked();  
            return null;  
        }  
  
        CursorWindow window = mCursor.getWindow();  
        if (window != null) {  
            closeFilledWindowLocked();  
        } else {  
            window = mFilledWindow;  
            if (window == null) {  
                mFilledWindow = new CursorWindow(mProviderName);  
                window = mFilledWindow;  
            } else if (position < window.getStartPosition()  
                    || position >= window.getStartPosition() + window.getNumRows()) {  
                window.clear();  
            }  
            mCursor.fillWindow(position, window);  
        }  
  
        if (window != null) {  
	        window.acquireReference();  
        }  
        return window;  
    }  
}

我们可以看到此处会调用fillWindow,而此处的mCursorCrossProcessCursorWrapper的实例,fillWindow则会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override  
public void fillWindow(int position, CursorWindow window) {  
    if (mCursor instanceof CrossProcessCursor) {  
        final CrossProcessCursor crossProcessCursor = (CrossProcessCursor)mCursor;  
        crossProcessCursor.fillWindow(position, window);  
        return;  
    }  
  
    DatabaseUtils.cursorFillWindow(mCursor, position, window);  
}

这样就会把每一个postion的数据填充到CursorWindow当中,但是这样会有个问题,为什么客户端能直接拿到呢。我们可以看看CursorWindow的内部。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public CursorWindow(String name, @BytesLong long windowSizeBytes) {  
    if (windowSizeBytes < 0) {  
        throw new IllegalArgumentException("Window size cannot be less than 0");  
    }  
    mStartPos = 0;  
    mName = name != null && name.length() != 0 ? name : "<unnamed>";  
    mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);  
    if (mWindowPtr == 0) {  
        throw new AssertionError(); 
    }  
    mCloseGuard.open("CursorWindow.close");  
}

我们从这里可以看到,CursorWindow保存数据并没有直接放在java中的,而是在natvie中实现的。我们可以在CursorWindow.cpp中找到nativeCreate的实现,我们在其中会发现如下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//CursorWindow::create
status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) {  
    *outWindow = nullptr;  
  
    CursorWindow* window = new CursorWindow();  
    if (!window) goto fail;  
  
    window->mName = name;  
    window->mSize = std::min(kInlineSize, inflatedSize);  
    window->mInflatedSize = inflatedSize;  
    window->mData = malloc(window->mSize);  
    if (!window->mData) goto fail;  
    window->mReadOnly = false;  
  
    window->clear();  
    window->updateSlotsData();  
  
    *outWindow = window;  
    return OK;  
  
fail:  
    LOG(ERROR) << "Failed create";  
fail_silent:  
    delete window;  
    return UNKNOWN_ERROR;  
}

但是直接用内存的话,如果我们改变CursorWindow内的数据的时候,在使用端是没办法直接拿到更新的数据的。其实在给插入数据的时候,调用maybeInflate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
status_t CursorWindow::maybeInflate() {  
    int ashmemFd = 0;  
    void* newData = nullptr;  
  
    // Bail early when we can't expand any further  
    if (mReadOnly || mSize == mInflatedSize) {  
        return INVALID_OPERATION;  
    }  
  
    String8 ashmemName("CursorWindow: ");  
    ashmemName.append(mName);  
  
    ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize);  
    ...
  
    newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);  
    ...
  
    {  
        // Migrate existing contents into new ashmem region  
        uint32_t slotsSize = sizeOfSlots();  
        uint32_t newSlotsOffset = mInflatedSize - slotsSize;  
        memcpy(static_cast<uint8_t*>(newData),  
                static_cast<uint8_t*>(mData), mAllocOffset);  
        memcpy(static_cast<uint8_t*>(newData) + newSlotsOffset,  
                static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);  
  
        free(mData);  
        mAshmemFd = ashmemFd;  
        mData = newData;  
        mSize = mInflatedSize;  
        mSlotsOffset = newSlotsOffset;  
  
        updateSlotsData();  
    }  
  
    return OK;  
...
}

从代码可以看到,当我们不是只读模式,且size不是和inflateSize相同的时候,会去创建匿名内存,把原来的数据复制的新的匿名内存中去。而会把匿名内存的FD保存到mAshmemFd当中。这样在客户端就可以拿到这个fd,从而可以读取到数据。因为这样做,也只是把fd和CursorWindow的一些基本信息从服务端传到了Client,这样服务端往匿名内存中写数据,客户端也就可以拿到其中的数据了。这样做既可以减少Binder调用的数据量,也可以解决掉Binder传输有1MB的限制。为了验证我们的想法,可以看看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
status_t CursorWindow::writeToParcel(Parcel* parcel) {  
    LOG(DEBUG) << "Writing to parcel: " << this->toString();  
  
    if (parcel->writeString8(mName)) goto fail;  
    if (parcel->writeUint32(mNumRows)) goto fail;  
    if (parcel->writeUint32(mNumColumns)) goto fail;  
    if (mAshmemFd != -1) {  
        if (parcel->writeUint32(mSize)) goto fail;  
        if (parcel->writeBool(true)) goto fail;  
        if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail;  
    } else {  
        // Since we know we're going to be read-only on the remote side,  
        // we can compact ourselves on the wire.        size_t slotsSize = sizeOfSlots();  
        size_t compactedSize = sizeInUse();  
        if (parcel->writeUint32(compactedSize)) goto fail;  
        if (parcel->writeBool(false)) goto fail;  
        void* dest = parcel->writeInplace(compactedSize);  
        if (!dest) goto fail;  
        memcpy(static_cast<uint8_t*>(dest),  
                static_cast<uint8_t*>(mData), mAllocOffset);  
        memcpy(static_cast<uint8_t*>(dest) + compactedSize - slotsSize,  
                static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);  
    }  
    return OK;  
  
fail:  
    LOG(ERROR) << "Failed writeToParcel";  
fail_silent:  
    return UNKNOWN_ERROR;  
}

可以看到在服务端转成Parcel的时候,是写入了name,numRow,numColumn, size, ashMemFd这些,同样,在客户端也会读取这些东西。代码就不贴了。

下面再理一下执行onMoveToNext时候的流程。在客户端的调用如下:

sequenceDiagram
box LightYellow
participant App
participant CursorWrapperInner
participant BulkCursorToCursorAdaptor
participant BulkCursorProxy
end
App->>+CursorWrapperInner: moveToNext
CursorWrapperInner->>BulkCursorToCursorAdaptor: onMove
BulkCursorToCursorAdaptor->>BulkCursorProxy: onMove
BulkCursorProxy->>Remote(CursorToBulkCursorAdapter): binder(ON_MOVE_TRANSACTION)
CursorWrapperInner-->>-App: finishMoveToNext

服务端的调用如下,前面的onMove调用在使用SQLiteCursor的时候会有一些不同,这里以读取SQLite数据库为例,内容有简化:

sequenceDiagram

(Client)BulkCursorProxy->>CursorToBulkCursorAdapter: binder(ON_MOVE_TRANSACTION)
CursorToBulkCursorAdapter->>CrossProcessCursorWrapper: onMove
CrossProcessCursorWrapper->>SQLiteCursor: onMove
SQLiteCursor->>SQLiteCursor: fillWindow
SQLiteCursor->>SQLiteConnection: executeForCursorWindow
SQLiteConnection->>CursorWindow: putXX
note right of SQLiteConnection: native 写入数据到CursorWindow


box LightGreen
participant CursorToBulkCursorAdapter
participant CrossProcessCursorWrapper
participant SQLiteCursor
participant SQLiteConnection
participant CursorWindow
end

ContentObserver监听的注册

当我们想要监听一个ContentProvider的变化时,可以按照如下的方法创建一个ContentObserver,并调用registerContentObserver来注册监听,通过传入的Uri来设置指定的数据源,通过Uri的path可以设置监听指定数据源中的某一部分数据的变化。

1
2
3
4
5
6
7
val contentObserver = object: ContentObserver(Handler.getMain()) {  
    override fun onChange(selfChange: Boolean) {  
        super.onChange(selfChange)  
        //do something while receive onChange  
    }  
}  
contentResolver.registerContentObserver(Uri.parse("content://sms"), true, contentObserver)

我们继承的这个ContentObserver有一个内部类Transport,它实现了 IContentObserver.Stub, 这个和ContentProvider的内部类实现类似,也是实现了binder的数据交互。registerContentObserver方法也在ContentResolver中,代码如下:

1
2
3
4
5
6
7
8
9
public final void registerContentObserver(Uri uri, boolean notifyForDescendents,  
        ContentObserver observer, @UserIdInt int userHandle) {  
    try {  
        getContentService().registerContentObserver(uri, notifyForDescendents,  
                observer.getContentObserver(), userHandle, mTargetSdkVersion);  
    } catch (RemoteException e) {  
        throw e.rethrowFromSystemServer();  
    }  
}

可以看到上面的代码调用了ContentServiceregisterContentObserver方法,这里拿到的是一个binder接口的实现IContentService的代理类,在binder另一端真正执行这个方法的是在ContentService中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override  
public void registerContentObserver(Uri uri, boolean notifyForDescendants,  
        IContentObserver observer, int userHandle, int targetSdkVersion) {  
    final int uid = Binder.getCallingUid();  
    final int pid = Binder.getCallingPid();  
    userHandle = handleIncomingUser(uri, pid, uid,  
            Intent.FLAG_GRANT_READ_URI_PERMISSION, true, userHandle);  
  
    final String msg = LocalServices.getService(ActivityManagerInternal.class)  
            .checkContentProviderAccess(uri.getAuthority(), userHandle);  
    ...
    synchronized (mRootNode) {  
        mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,  
                uid, pid, userHandle);  
    }  
}

服务端的主要代码如上,其中传过来的observer为IContentObserver,它就是我们刚刚说到的和Transport相同的接口。其中主要就是调用了mRootNode.addObserverLockedmRootNode是内部类ObserverNode的实例。继续看代码之前先介绍一下这个类,这个类内部又有ObserverEntry内部类,我们的Observer会存放到这个Entry内部。ObserverNode有两个成员mChildrenmObservers,分别表示子一级的ObserverNode和当前路径级的ObserverEntry,从而组成了如下的树状结构。

addObserverLocked这个方法的代码我们也能知晓该树状结构的构成,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void addObserverLocked(Uri uri, int index, IContentObserver observer,  
                               boolean notifyForDescendants, Object observersLock,  
                               int uid, int pid, int userHandle) {  
    // If this is the leaf node add the observer  
    if (index == countUriSegments(uri)) {  
        mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,  
                uid, pid, userHandle, uri));  
        return;  
    }  
  
    // Look to see if the proper child already exists  
    String segment = getUriSegment(uri, index);  
    if (segment == null) {  
        throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer");  
    }  
    int N = mChildren.size();  
    for (int i = 0; i < N; i++) {  
        ObserverNode node = mChildren.get(i);  
        if (node.mName.equals(segment)) {  
            node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,  
                    observersLock, uid, pid, userHandle);  
            return;  
        }  
    }  
  
    // No child found, create one  
    ObserverNode node = new ObserverNode(segment);  
    mChildren.add(node);  
    node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,  
            observersLock, uid, pid, userHandle);  
}

这样操作完,也就完成了添加Observer的操作。

数据更新的发布与分发

那数据更新的部分呢,当我们执行了数据整删改之后,需要调用如下代码通知数据变化:

1
getContext().getContentResolver().notifyChange(uri, null);

这个notifyChange方法有几个实现,最终会调用到如下这个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void notifyChange(@NonNull Uri[] uris, ContentObserver observer, @NotifyFlags int flags,  
        @UserIdInt int userHandle) {  
    try {  
        getContentService().notifyChange(  
                uris, observer == null ? null : observer.getContentObserver(),  
                observer != null && observer.deliverSelfNotifications(), flags,  
                userHandle, mTargetSdkVersion, mContext.getPackageName());  
    } catch (RemoteException e) {  
        throw e.rethrowFromSystemServer();  
    }  
}

其中getContentService会获取到IContentService在本地的代理,而最终会通过Binder调用到system_server中的ContentService中的notifyChange方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Override  
public void notifyChange(Uri[] uris, IContentObserver observer,  
        boolean observerWantsSelfNotifications, int flags, int userId,  
        int targetSdkVersion, String callingPackage) {  
  
    final int callingUid = Binder.getCallingUid();  
    final int callingPid = Binder.getCallingPid();  
    final int callingUserId = UserHandle.getCallingUserId();  
  
    
    final ObserverCollector collector = new ObserverCollector();  
  
   
  
    for (Uri uri : uris) {  
        final int resolvedUserId = handleIncomingUser(uri, callingPid, callingUid,  
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION, true, userId);  
        final Pair<String, Integer> provider = Pair.create(uri.getAuthority(), resolvedUserId);  
        if (!validatedProviders.containsKey(provider)) {  
            final String msg = LocalServices.getService(ActivityManagerInternal.class)  
                    .checkContentProviderAccess(uri.getAuthority(), resolvedUserId);  
            if (msg != null) {  
                if (targetSdkVersion >= Build.VERSION_CODES.O) {  
                    throw new SecurityException(msg);  
                } else {  
                    if (msg.startsWith("Failed to find provider")) {  
                        // Sigh, we need to quietly let apps targeting older API  
                      } else {  
                        Log.w(TAG, "Ignoring notify for " + uri + " from "  
                                + callingUid + ": " + msg);  
                        continue;  
                    }  
                }  
            }  
  
            // Remember that we've validated this access  
            final String packageName = getProviderPackageName(uri, resolvedUserId);  
            validatedProviders.put(provider, packageName);  
        }  
  
        synchronized (mRootNode) {  
            final int segmentCount = ObserverNode.countUriSegments(uri);  
            mRootNode.collectObserversLocked(uri, segmentCount, 0, observer,  
                    observerWantsSelfNotifications, flags, resolvedUserId, collector);  
        }  
    }  
  
    final long token = clearCallingIdentity();  
    try {  
        // Actually dispatch all the notifications we collected  
        collector.dispatch();  
        .....
        }  
    } finally {  
        Binder.restoreCallingIdentity(token);  
    }  
}

以上代码略有简化,只保留了和ContentObserver通知相关的代码,SyncManager相关的代码未放在这里。这段代码最开始是先遍历传入的Uri列表,对检查对应Uri的ContentProvider是否有权限,如果有权限则会调用collectObserversLocked把满足条件的Observer放到ObserverCollector中去,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//ObserverNode.collectObserversLocked
public void collectObserversLocked(Uri uri, int segmentCount, int index,  
        IContentObserver observer, boolean observerWantsSelfNotifications, int flags,  
        int targetUserHandle, ObserverCollector collector) {  
    String segment = null;  
    if (index >= segmentCount) {  
        // This is the leaf node, notify all observers  
        collectMyObserversLocked(uri, true, observer, observerWantsSelfNotifications,  
                flags, targetUserHandle, collector);  
    } else if (index < segmentCount){  
        segment = getUriSegment(uri, index);  
        // Notify any observers at this level who are interested in descendants 
        collectMyObserversLocked(uri, false, observer, observerWantsSelfNotifications,  
                flags, targetUserHandle, collector);  
    }  
  
    int N = mChildren.size();  
    for (int i = 0; i < N; i++) {  
        ObserverNode node = mChildren.get(i);  
        if (segment == null || node.mName.equals(segment)) {  
            // We found the child,  
            node.collectObserversLocked(uri, segmentCount, index + 1, observer,  
                    observerWantsSelfNotifications, flags, targetUserHandle, collector);  
            if (segment != null) {  
                break;  
            }  
        }  
    }  
}

传入的segmentCount为Uri的path的数量加上authority的数量,比如content://sms/inbox这个uri它的segmentCount就是2,而从外面传如的index为0。它会先调用collectMyObserversLocked方法来遍历当前Node层级的ObserverEntry,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void collectMyObserversLocked(Uri uri, boolean leaf, IContentObserver observer,  boolean observerWantsSelfNotifications, int flags, int targetUserHandle, ObserverCollector collector) {  
    int N = mObservers.size();  
    IBinder observerBinder = observer == null ? null : observer.asBinder();  
    for (int i = 0; i < N; i++) {  
        ObserverEntry entry = mObservers.get(i);   
       boolean selfChange = (entry.observer.asBinder() == observerBinder);  
        if (selfChange && !observerWantsSelfNotifications) {  
            continue;  
        }  
  
        // Does this observer match the target user?  
        if (targetUserHandle == UserHandle.USER_ALL  
                || entry.userHandle == UserHandle.USER_ALL  
                || targetUserHandle == entry.userHandle) {  
            // Make sure the observer is interested in the notification  
            if (leaf) {  
                 if ((flags&ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS) != 0  && entry.notifyForDescendants) {  
                    continue;  
                }  
            } else {  
                if (!entry.notifyForDescendants) {  
                    continue;  
                }  
            }   
            collector.collect(entry.observer, entry.uid, selfChange, uri, flags,  targetUserHandle);  
        }  
    }  
}

可以看到以上代码就是遍历我们之前注册observer时候的mObservers列表,分别检查了用户id是否相等,是否满足notifyForDescendants和flag等参数后调用collector.collect方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void collect(IContentObserver observer, int uid, boolean selfChange, Uri uri,  
        int flags, int userId) {  
    final Key key = new Key(observer, uid, selfChange, flags, userId);  
    List<Uri> value = collected.get(key);  
    if (value == null) {  
        value = new ArrayList<>();  
        collected.put(key, value);  
    }  
    value.add(uri);  
}

Collector中则是以observer,uid,selfChange,flag,userId组合成key,Uri作为value放入collectedmap中。而这些只是完成了一个层级的Observer收集,collectObserversLocked方法中还会遍历mChildren,找到其中的name与segment相同的子Node,再进行收集。收集完成之后,则是调用collector.dispatch(),看名字就知道是去通知对应的Observer,具体实现逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void dispatch() {  
    for (int i = 0; i < collected.size(); i++) {  
        final Key key = collected.keyAt(i);  
        final List<Uri> value = collected.valueAt(i);  
  
        final Runnable task = () -> {  
            try {  
                key.observer.onChangeEtc(key.selfChange,  
                        value.toArray(new Uri[value.size()]), key.flags, key.userId);  
            } catch (RemoteException ignored) {  
            }  
        };      
        final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;  
        final int procState = LocalServices.getService(ActivityManagerInternal.class)  
                .getUidProcessState(key.uid);  
        if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || noDelay) {  
            task.run();  
        } else {  
            BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY);  
        }  
    }  
}

这个方法则是遍历collected这个ArrayMap,从每一个key当中取出observer,并使用Runnable封装,在根据flags和当前的进程状态决定是立即通知变化还是延迟通知变化。而这里所调用的onChangeEtc则会通过Binder调用,从而调用到客户端的Observer。

这便完成了ContentProvider内容变化的通知。

总结

本文介绍了使用ContentProvider进行数据的查询、查处来的数据进行窗口移动、注册数据变化监听以及数据变化接受这几块的代码分析,加上前面两篇关于ContentProvider的文章,基本上可以对于ContentProvider整个体系有详细的了解。整删改查这四种操作中,查是比较复杂的,把它看完,增删改这三种流程,想要看明白就会简单很多,因此这里也便不再分析了。

从Android ContentProvider的设计和代码实现中我们可以学到很多东西。其中之一是前面介绍到IContentProvider在自己调用和其他App调用的区别,以及对于代码的巧妙封装,使得代码的实现比较优雅,同时代码量比较少,对于同UID应用来说性能又比较优。另外就是通过CursoWindow的实现,突破Binder数据传输的限制。ObserverNode中使用树来实现了Observer监听。

当然这只是我的一人之言,因为关注点不同,在看代码的过程中还是会有一些细节被我忽略,但是可能对于其他人来说又比较重要的。如果你对ContentProvider也有自己的见解,又或是我有错误的解读,欢迎留言交流。

看完评论一下吧

Android源码分析: 使用场景获取ContentProvider分析

2024-08-27 10:22:03

之前已经分析过在应用启动的时候安装ContentProvider的流程了,现在我们再从使用者的角度看看是怎样去拿到ContentProvider的。

在使用ContentProvider的时候,我们通常会使用Context拿到ContentResolver,然后在执行CURD的操作,比如我们要查询手机中的联系人,通常会这样做:

1
2
3
4
5
6
7
8
String[] projection = new String[]    {
	Profile._ID,        
	Profile.DISPLAY_NAME_PRIMARY,        
	Profile.LOOKUP_KEY,        
	Profile.PHOTO_THUMBNAIL_URI    };  
Cursor profileCursor = getContentResolver().query(
Profile.CONTENT_URI,
projection , null, null,null);

这里先重点分析一下拿到ContentProvider的过程。首先来看看这个ContentResolver是什么东西。通过源码我们可以看到它是一个抽象类,实现了ContentInterface接口,ContentInterface中则定义了CRUD的相关方法。我们可以在ContextImpl中找到getContentResolver(),通过源码我们知道,实际上我们拿到的是ApplicationContentResolver对象。

这里看完,我们可以继续看query方法,实现在ContentResolver类当中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
IContentProvider unstableProvider = acquireUnstableProvider(uri);
IContentProvider stableProvider = null;  
Cursor qCursor = null;  
try {
	try {  
	    qCursor = unstableProvider.query(mContext.getAttributionSource(), uri, projection,  
            queryArgs, remoteCancellationSignal);  
	} catch (DeadObjectException e) {
		unstableProviderDied(unstableProvider);  
		stableProvider = acquireProvider(uri);  
		if (stableProvider == null) {  
		    return null;  
		}  
		qCursor = stableProvider.query(mContext.getAttributionSource(), uri, projection,  
        queryArgs, remoteCancellationSignal);
	}
	qCursor.getCount();
	final IContentProvider provider = (stableProvider != null) ? stableProvider  
        : acquireProvider(uri);  
	final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);  
	stableProvider = null;  
	qCursor = null;  
	return wrapper;
} catch (RemoteException e) {
	return null;
} finally {
	if (qCursor != null) {  
	    qCursor.close();  
	}
	if (unstableProvider != null) {  
	    releaseUnstableProvider(unstableProvider);  
	}  
	if (stableProvider != null) {  
	    releaseProvider(stableProvider);  
	}
}

上面的代码看起来还是比较简单的,首先是是去调用acquireUnstableProvider拿到unstableProvider,通过它去取数据,如果拿不到再去调用acquireProviderstableProvider,最后把stableProvider和数据使用CursorWrappInner包装返回给调用者,在finally中把cursor关掉,把provider给释放掉。

我们先来看看拿stableProvider的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final IContentProvider acquireUnstableProvider(Uri uri) {  
    if (!SCHEME_CONTENT.equals(uri.getScheme())) {  
        return null;  
    }  
    String auth = uri.getAuthority();  
    if (auth != null) {  
        return acquireUnstableProvider(mContext, uri.getAuthority());  
    }  
    return null;  
}

简单说一下,上面首先会判断我们的URL是否为content:开头,因为这是ContentProvider的scheme。之后会到url中拿到authority, autority包括这几个部分:[userinfo@]host[:port] 。最后通过authority去调用ApplicationContentResolver中的同名方法。

1
2
3
4
5
protected IContentProvider acquireUnstableProvider(Context c, String auth) {  
    return mMainThread.acquireProvider(c,  
            ContentProvider.getAuthorityWithoutUserId(auth),  
            resolveUserIdFromAuthority(auth), false);  
}

上面的方法会从我们的authority分别拿出userId和host,当然userId有可能是不传的,就会默认使用当前用户。我们继续去看ActivityThread.acquireProvider()代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final IContentProvider acquireProvider(  
        Context c, String auth, int userId, boolean stable) {  
    final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);  
    if (provider != null) {  
        return provider;  
    }
    ContentProviderHolder holder = null;
    final ProviderKey key = getGetProviderKey(auth, userId);
    try {
	    synchronized (key) {
		    holder = ActivityManager.getService().getContentProvider( getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
		    if (holder != null && holder.provider == null && !holder.mLocal) {
			    synchronized (key.mLock) {
				    if(key.mHolder != null) {
				    } else {
					    key.mLock.wait(ContentResolver.CONTENT_PROVIDER_READY_TIMEOUT_MILLIS)
				    }
				    holder = key.mHolder;
			    }
		    }
	    }
    } finally {
	    synchronized (key.mLock) {  
		    key.mHolder = null;  
		}
    }
    holder = installProvider(c, holder, holder.info, true, holder.noReleaseNeeded, stable);
    return holder.provider;
}

这里我们有一个参数stable,因此我们前面获取stableProviderunstableProvider都会走到这个方法里面来。 第3行代码,我们首先会到已存在的Provider列表中去拿,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final IContentProvider acquireExistingProvider(  
        Context c, String auth, int userId, boolean stable) {  
    synchronized (mProviderMap) {  
        final ProviderKey key = new ProviderKey(auth, userId);  
        final ProviderClientRecord pr = mProviderMap.get(key);  
        if (pr == null) {  
            return null;  
        }  
  
        IContentProvider provider = pr.mProvider;  
        IBinder jBinder = provider.asBinder();  
        if (!jBinder.isBinderAlive()) {  
	        //处理Binder不存活的情况
            handleUnstableProviderDiedLocked(jBinder, true);  
            return null;  
        }  
  
        ProviderRefCount prc = mProviderRefCountMap.get(jBinder);  
        if (prc != null) {  
            incProviderRefLocked(prc, stable);  //增加引用计数
        }  
        return provider;  
    }  
}

可以看到此处为通过auth构建出来的key到mProviderMap中查找ProviderClientRecord,而这个就是我们之前分析安装Provider时候所创建并且放置到这个map中去的。后面会检查Binder是否仍然存活,并返回。 在这里我们需要注意一点,如果安装我们之前分析安装的流程,我们在自己的app里面拿自己的ContentProvider这里是肯定可以拿到的,但是如果是其他的应用提供的ContentProvider这里很显然是拿不到的。因此我们需要继续回到acquireProvider方法去看其他部分的代码。

在第11行中,我们会到AMS中去获取ContentProviderHolder,如果拿到了远端的holder,但是我们本地的ProviderKey中的holder为空,说明我们本地还没有安装这个ContentProvider,需要等待,也就是执行第16行代码进入等待状态。而这个地方的解除等待在ContentProviderHelper类的publishContentProviders方法中,可以去之前分析安装过程的文章最后一部分查看。

而拿到holder之后,最后又去执行了一次installProvider方法,这里的安装跟我们之前的启动App安装是有一些不同的,我们放到后面再来分析。

然而前面的去AMS拿ContentProviderHolder代码我们还没有看,具体代码也仍然在ContentProviderHelper中,现在去看一下它的getContentProviderImpl()方法,内容比较长,先一点一点的贴代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//ContentProviderHelper.java getContentProviderImpl
synchronized (mService) {
	ProcessRecord r = null;  
	if (caller != null) {  
	    r = mService.getRecordForAppLOSP(caller);
    }

	UserManagerService userManagerService = UserManagerService.getInstance();
	if (!isAuthorityRedirectedForCloneProfile(name)  
        || !userManagerService.isMediaSharedWithParent(userId)) {  //。mediastore需要特殊判断,这里会把那些情况给过滤掉
	    cpr = mProviderMap.getProviderByName(name, userId);  
	}
	...
	
	ProcessRecord dyingProc = null;  
	if (cpr != null && cpr.proc != null) {  
	    providerRunning = !cpr.proc.isKilled(); //检查ContentProvider目标进程是否被杀掉
	    if (cpr.proc.isKilled() && cpr.proc.isKilledByAm()) {   
		    dyingProc = cpr.proc;   //如果被杀了或者正在被杀就记录
		}
	}

	if (providerRunning) {
		cpi = cpr.info;
		if (r != null && cpr.canRunHere(r)) {
			checkAssociationAndPermissionLocked(r, cpi, callingUid, userId, checkCrossUser,  
	        cpr.name.flattenToShortString(), startTime);
	        ContentProviderHolder holder = cpr.newHolder(null, true);
	        holder.provider = null;
			return holder;
		}
		//PLACEHOLDER1
	}
	//PLACEHOLDER2
}

以上的代码是我们会遇到的第一种情况,首先去拿到进程ProcessRecord,之后根据Provider的authority name和userId到ProviderMap中拿已有的ContentProviderRecord。拿到之后首先检查ContentProvider提供方的进程是否正在运行中,如果在运行中,并且canRunHere检查为true, 就会检查是否有权限来执行,有权限就会创建一个ContentProviderHolder传递出去。 canRunHere所做的判断代码如下:

1
2
3
4
public boolean canRunHere(ProcessRecord app) {  
    return (info.multiprocess || info.processName.equals(app.processName))  
            && uid == app.info.uid;  
}

解释下就是首先判断Provider是否支持多个进程中运行,也就是在Manifest为provider配置了multiprocess=true,另外检查Provider所在进程和当前调用是否为同一个进程,这两者条件满足一个就可以。同时还要满足当前进程的UID和Provider的进程UID相同,这个在两者为同一个应用,或者两者共享签名,或共享UID的情况下满足。这种情况下就可以直接使用ContentProvider。这种情况会创建新的ContentProviderHolder传递到App进程,其中会携带ContentProviderRecord过去。此时我们看到的传到App进程的ContentProviderConnection也是为空,至于这个对象的用处是什么我们后面会分析。同时还会把Holder的成员provider设置为空,这个有什么用呢,可以后面再看installProvider方法。

在这里还有一个检查权限和是否可以联合运行的方法checkAssociationAndPermissionLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if ((msg = checkContentProviderAssociation(callingApp, callingUid, cpi)) != null) {  
    throw new SecurityException("Content provider lookup " + cprName  
            + " failed: association not allowed with package " + msg);  
}

if ((msg = checkContentProviderPermission(  
            cpi, Binder.getCallingPid(), Binder.getCallingUid(), userId, checkUser,  
            callingApp != null ? callingApp.toString() : null))  
        != null) {  
    throw new SecurityException(msg);  
}

里面又分别调用了两个方法,第一个用于检查两个进程是否可以联合使用,默认是允许的,除非是系统内置应用或者预装应用会有比较严格的检查,我们这里不必关注。可以去看一下权限检查,这个比较重要:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private String checkContentProviderPermission(ProviderInfo cpi, int callingPid, int callingUid,  
        int userId, boolean checkUser, String appName) {  
    boolean checkedGrants = false;  
    if (checkUser) { //对于普通应用这个值传过来的为true
	    int tmpTargetUserId = mService.mUserController.unsafeConvertIncomingUser(userId);  
		if (tmpTargetUserId != UserHandle.getUserId(callingUid)) {  
			//检查是否有临时授权,这个一般是在Manifest中添加<grant-uri-permission>或者android:grantUriPermissions
		    if (mService.mUgmInternal.checkAuthorityGrants(  
            callingUid, cpi, tmpTargetUserId, checkUser)) {  
		        return null;  //检查通过直接返回成功
		    }  
		    checkedGrants = true; 
		}
		userId = mService.mUserController.handleIncomingUser(callingPid, callingUid, userId,  
        false, ActivityManagerInternal.ALLOW_NON_FULL,  
        "checkContentProviderPermissionLocked " + cpi.authority, null);
        if (userId != tmpTargetuserId) {
	        checkGrants = false;
        }
    }
    if (ActivityManagerService.checkComponentPermission(cpi.readPermission,  
        callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)  
        == PackageManager.PERMISSION_GRANTED) {  //检查读权限,授权过返回
	    return null;  
	}
	if (ActivityManagerService.checkComponentPermission(cpi.writePermission,  
        callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)  
        == PackageManager.PERMISSION_GRANTED) {  //写权限检查,授权过则返回成功
	    return null;  
	}
	PathPermission[] pps = cpi.pathPermissions;  
if (pps != null) {  
    int i = pps.length;  
    while (i > 0) {  
        i--;  
        PathPermission pp = pps[i];  
        String pprperm = pp.getReadPermission();  
        if (pprperm != null && ActivityManagerService.checkComponentPermission(pprperm,  
                callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)  
                == PackageManager.PERMISSION_GRANTED) {  
            return null;  
        }  
        String ppwperm = pp.getWritePermission();  
        if (ppwperm != null && ActivityManagerService.checkComponentPermission(ppwperm,  
                callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)  
                == PackageManager.PERMISSION_GRANTED) {  
            return null;  
        }  
    }  
}
    
}

关于权限,前面的代码我已经加了相关的注释,我们可以对比官方文档,其中共检查了四种权限,分别是临时授权,路径授权,单独的读写授权和单一读写程序级别的授权。关于权限检查的更多内容,这里我们也先略过。此时我们可以继续回来继续分析getContentProviderImpl方法。我们继续看上面留的PLACEHOLDER 1处的代码:

1
2
3
4
5
6
checkAssociationAndPermissionLocked(r, cpi, callingUid, userId, checkCrossUser, 
        cpr.name.flattenToShortString(), startTime);
conn = incProviderCountLocked(r, cpr, token, callingUid, callingPackage,  
        callingTag, stable, true, startTime, mService.mProcessList,  
        expectedUserId);
        

其中还有一些关于OOM设置的代码这里先跳过了,上面主要的代码也是检查权限以及这个incProviderCountLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private ContentProviderConnection incProviderCountLocked(ProcessRecord r,  
        final ContentProviderRecord cpr, IBinder externalProcessToken, int callingUid,  
        String callingPackage, String callingTag, boolean stable, boolean updateLru,  
        long startTime, ProcessList processList, @UserIdInt int expectedUserId) {  
    final ProcessProviderRecord pr = r.mProviders;  
    for (int i = 0, size = pr.numberOfProviderConnections(); i < size; i++) {  
        ContentProviderConnection conn = pr.getProviderConnectionAt(i);  
        if (conn.provider == cpr) {  
            conn.incrementCount(stable);  
            return conn;  
        }  
    }  
  
    ContentProviderConnection conn = new ContentProviderConnection(cpr, r, callingPackage,  
            expectedUserId);  
    conn.startAssociationIfNeeded();  
    conn.initializeCount(stable);  
    cpr.connections.add(conn);  
    if (cpr.proc != null) {  
        cpr.proc.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_PROVIDER);  
    }  
    pr.addProviderConnection(conn);  
    mService.startAssociationLocked(r.uid, r.processName, r.mState.getCurProcState(),  
            cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName);  
    if (updateLru && cpr.proc != null  
            && r.mState.getSetAdj() <= ProcessList.PERCEPTIBLE_LOW_APP_ADJ) {  
        processList.updateLruProcessLocked(cpr.proc, false, null);  
    }  
    return conn;  
}

这里有不少关于Association相关的代码,而我们的应用一般不会走到这里。我们只需要关注其中创建Connection以及为他创建引用计数。关于它的计数,我们放到最好再看一下。

PLACEHOLDER 2处,首先处理的就是provider为运行的情况,这种情况就会回到Provider的进程去安装ContentProvider,这部分代码我们之前已经分析过了,这里略过。而我们是在使用者进程调用的此处的caller也不为空,再往后,则应该是如下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
mService.grantImplicitAccess(userId, null, callingUid,  
        UserHandle.getAppId(cpi.applicationInfo.uid));

if (caller != null) {
	synchronized (cpr) {  
	    if (cpr.provider == null) {  
	        if (cpr.launchingApp == null) {  
	            return null;  
	        }  
  
	        if (conn != null) {  
	            conn.waiting = true;  
	        }  
	    }  
	} 
	return cpr.newHolder(conn, false);
}

这里可以看到,就是先给调用的uid授权,设置wait 为true,创建一个ContentProviderHolder返回。这里是带着ContentProviderConnectionIContentProvider的。

代码讲解的部分只介绍了我们认为caller不为空的情况,实际上是更加复杂的,这里就把其中的完整流程流程图放在这里,如有需要可参考流程图以及之前的App启动时候的ContentProvider安装一起看。

---
title: getContentProviderImpl流程
---
flowchart TD
A(getContentProviderImpl) --> B(mProviderMap.getProviderByName)
B --> C(providerRunning = !cpr.proc.isKilled)
C --> D{Check providerRunning}
D --> |providerRunning == true|E{cpr.canRunHere}
E --> |No| I{CheckPermission}
E --> |Yes|F{ChecPermission}
F --> |Pass|G((Return local Holder))
F --> |Not Pass|H(Throw Exception)
I --> |Pass|J(incProviderCountLocked)
I --> |Not Pass|H
D --> |No|K(PMS.resolveContentProvider)
K --> L{CheckPermission}
L --> |Not Pass|H
L --> |Pass|M(Generate CPRecord)
M --> A1{cpr.canRunHere}
A1 --> |true|A2((Return local Holder))
A1 --> |False| A3{Process Live}
A3 --> |Process is live|A4(Install Provider)
A3 --> |Not Start Or Die| A5(Start Process)
A4 --> A6(incProviderCountLocked)
A5 --> A6
A6 --> B1(AMS.grantImplictAccess)
J --> B1
B1 --> B2{From customer Call}
B2 --> |Yes|B3((Return Remote Holder))
B2 --> |No|B4{cpr.provider==null}
B4 --> |Yes|B5((cpr.wait))
B5 --> B4
B4 --> |No|B6((Return Remote Holder))

看了这么多,我们就可以继续回去看App进程的代码了。在App进程就是执行我们前面说的installProvider过程。 我们可以继续分析query的过程,看代码我们知道调用的是IContentProvider的query方法,对于同UID的进程,IContentProvider为我们在instalProvider创建的本地的ContentProvider中的mTransport而其他的则是AMS调用带过来的IcontentProvider远端接口,我们这里以非本进程的情况来分析,它的获取是如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//android.content.ContentProviderNative.java
static public IContentProvider asInterface(IBinder obj)  
{  
    if (obj == null) {  
        return null;  
    }  
    IContentProvider in =  
        (IContentProvider)obj.queryLocalInterface(descriptor);  
    if (in != null) {  
        return in;  
    }  
  
    return new ContentProviderProxy(obj);  
}

也就是说,如果是相同的UID的进程拿到的为Transport对象,如果是其他的则拿到的是ContentProviderProxy对象。

前面我们还有关于ContentProviderConnection还有很多东西没有介绍,这里继续看一下。首先是incProviderCountLocked方法中所调用的conn.incrementCount(stable)。在我看代码的过程中stable这个变量唯有这里使用了,我们继续看它的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public int incrementCount(boolean stable) {  
    synchronized (mLock) {  
        if (stable) {  
            mStableCount++;  
            mNumStableIncs++;  
        } else {  
            mUnstableCount++;  
            mNumUnstableIncs++;  
        }  
        return mStableCount + mUnstableCount;  
    }  
}

可以看到这个类主要记录了Stable和UnStable的调用次数,实际上AMS这一端stable和unstable似乎除了计数之外没有什么区别。但是在客户端installProvider的时候却是有区别的。我们之前分析的启动时候安装的情况stable都是为true,我们可以看看ActivityThread.installProvider如下的代码:

1
2
3
4
5
6
7
8
if (noReleaseNeeded) {  
    prc = new ProviderRefCount(holder, client, 1000, 1000);  
} else {  
    prc = stable  
            ? new ProviderRefCount(holder, client, 1, 0)  
            : new ProviderRefCount(holder, client, 0, 1);  
}  
mProviderRefCountMap.put(jBinder, prc);

ProviderRefCount用于记录Provider的引用计数,其中用stableCount和unstableCount来计数,当我们不需要释放Provider的时候,两个数字都设置为了1000,当我们是stable的时候只设置stable数为1,unstable数量为0,当为unstable的时候也同理。之前我们是有看到对于已经存在的provider是通过incProviderRefLocked来增加起计数的。那我们有了增加计数,那么使用完之后也应该需要减少计数。在query的finally代码块中有如下代码:

1
2
3
4
5
6
if (unstableProvider != null) {  
    releaseUnstableProvider(unstableProvider);  
}  
if (stableProvider != null) {  
    releaseProvider(stableProvider);  
}

他们最终调用的为ActivityThread.releaseProvider方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public final boolean releaseProvider(IContentProvider provider, boolean stable) {  
    if (provider == null) {  
        return false;  
    }  
  
    IBinder jBinder = provider.asBinder();  
    synchronized (mProviderMap) {  
        ProviderRefCount prc = mProviderRefCountMap.get(jBinder);  
        if (prc == null) {  
            // The provider has no ref count, no release is needed.  
            return false;  
        }  
  
        boolean lastRef = false;  
        if (stable) {  
            if (prc.stableCount == 0) {  
                return false;  
            }  
            prc.stableCount -= 1;  
            if (prc.stableCount == 0) {  
                lastRef = prc.unstableCount == 0;  
                try {  
                   
                    ActivityManager.getService().refContentProvider(  
                            prc.holder.connection, -1, lastRef ? 1 : 0);  
                } catch (RemoteException e) {  
                    //do nothing content provider object is dead any way  
                }  
            }  
        } else {  
            if (prc.unstableCount == 0) {  
                return false;  
            }  
            prc.unstableCount -= 1;  
            if (prc.unstableCount == 0) {  
                lastRef = prc.stableCount == 0;  
                if (!lastRef) {  
                    try {  
                    
                        ActivityManager.getService().refContentProvider(  
                                prc.holder.connection, 0, -1);  
                    } catch (RemoteException e) {  
                        //do nothing content provider object is dead any way  
                    }  
                }  
            }  
        }  

        return true;  
    }  
}

代码主要分了两个分支,分别对stable和unstable的情况进行处理,他们都是先把本地对应的ProviderRefCount中的数字减一,但是调用AMS.refContentProvider却不一样,stable count减为0的时候会直接调用,而unstable为0的时候要stableCount不为0才会调用。传递的参数也有区别,代码很简单就不详解了。直接去看ContentProviderHelperrefContentProvider方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
boolean refContentProvider(IBinder connection, int stable, int unstable) {  
    ContentProviderConnection conn;  
    try {  
        conn = (ContentProviderConnection) connection;  
    } catch (ClassCastException e) {  
        
    }  
    if (conn == null) {  
        throw new NullPointerException("connection is null");  
    }  
  
    try {  
        conn.adjustCounts(stable, unstable);  
        return !conn.dead;  
    } finally {  
        
    }
}

这里的代码其实比较简单,就是调用ContentProviderConnectionadjustCounts,这个方法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void adjustCounts(int stableIncrement, int unstableIncrement) {  
    synchronized (mLock) {  
        if (stableIncrement > 0) {  
            mNumStableIncs += stableIncrement;  
        }  
        final int stable = mStableCount + stableIncrement;  
        if (stable < 0) {  
            throw new IllegalStateException("stableCount < 0: " + stable);  
        }  
        if (unstableIncrement > 0) {  
            mNumUnstableIncs += unstableIncrement;  
        }  
        final int unstable = mUnstableCount + unstableIncrement;  
        if (unstable < 0) {  
            throw new IllegalStateException("unstableCount < 0: " + unstable);  
        }  
        if ((stable + unstable) <= 0) {  
            throw new IllegalStateException("ref counts can't go to zero here: stable="  
                                            + stable + " unstable=" + unstable); 
        }  
        mStableCount = stable;  
        mUnstableCount = unstable;  
    }  
}

这里就是来根据传过来的参数来调整stableCountunstableCount,也就完成了这几个count的变化。也就是完成了AMS端的减少计数。

到此位置,我们也就拿到了IContentProvider,也就可以使用它提供的CRUD方法,进行数据的增删改查了。至于具体是如何查询数据,如何做到数据的跨进程共享,如何绕过Binder传输限制1MB实现跨进程传输数据,限于篇幅下次再来分析。

看完评论一下吧

在家搭建VaultWarden密码管理服务

2024-08-22 19:33:25

每个人都有很多密码,有人用脑记,有人用纸记,也有很多的工具帮我们记。之前我一直使用Keepass,在之前的文章介绍过。平时输入密码的场景最多的是网页中,目前keepass的网页插件只能说是能用的程度。前段时间给openwrt上面装上了docker,也想着在家搭一个密码管理服务,于是说干就干了。

密码服务能够个人搭建的就是Bitwarden了,因为在自家的树莓派上运行,因此选择了基于Rust的VaultWarden,毕竟资源消耗更少,性能也会更好点吧,它兼容Bitwarden,因此所有的客户端和浏览器插件都通用。 因为是在家里搭建要保证在外的时候,密码服务也能正常工作,因为已经有公网ip了,所以需要弄一个DDNS。因为搭建VaultWarden需要https,在查资料的过程中发现了lucky这个很好用的国产软件,索性把家里的DDNS和端口转发都换成了Lucky。

具体的流程就是首先在openwrt上面下载vaultwarden的镜像,因为国内docker默认镜像源用不了,所以我是用了github的镜像源:

1
docker pull ghcr.io/dani-garcia/vaultwarden:latest

之后在本地创建一个文件用来保存运行相关的环境变量:

ROCKET_PORT=1089
.....

当然了,也可以选择在运行docker的时候通过命令行带着,但是因为要加的变量很多,我就弄了个文件放。另外本地也要选择一个文件夹用来存放vaultwarden的数据。

1
docker run -d --name vaultwarden -v /data/vw-data:/data --network host --env-file /user/sam/env --restart unless-stopped  ghcr.io/dani-garcia/vaultwarden:latest

我这里的配置是通过环境变量指定了端口,然后docker里面使用宿主机的网络,而不是像官网文档那样用了桥接,至于原因则是因为在openwrt里面停了重启发现网卡被占用启动不了。按照如上步骤即可完成vaultwarden的启动了。

但是这样服务还是不能使用,因为没有https服务,vaultwarden还无法完成身份认证。因此需要使用lucky了,我们可以选择把它安装在openwrt上,也可以安装到docker里面,而我发现我华硕路由器的koolshare软件中心里面就有,遂决定把他放到路由器里面。

安装完lucky后,首先是到自己的域名解析服务商那里把二级域名弄好,因为自动申请证书和DDNS都需要,DNS最好使用Cloudflare,阿里云,DNSPod等几家可以通过api修改解析,lucky里面又内置了的,这样可以减少很多麻烦。搞好之后,就可以去lucky里面先弄证书自动申请了,当然有证书的可以直接添加进去,我这里用了ACME申请Let's Encrypt证书。入口在“安全管理里面”点击添加证书,更加具体的可以看官方文档,这里搞好之后,后面设置端口转发或者设置web服务的时候都会使用这个证书。

搞完SSL证书,我想到我这里其实不需要通过端口转发来实现,完全可以通过Lucky的web服务功能来做,于是就创建一条web服务的规则,如下:

监听端口为对外暴露的端口,TLS启用就开启网站的HTTPS功能,前提也要先配置好证书才能打开。默认规则中服务类型选择反向代理,目标地址就是我们的服务的地址,例如http://192.168.1.10:1089,万事大吉打开,这样有些header都能正确的传过去。

这一切都搞完,就可以去浏览起打开注册用户了。

为了安全起见,最好注册完之后把注册功能给关掉,做法就是修改环境变量。

SIGNUPS_ALLOWED=false

另外,Admin页面也是默认关闭的,我自认为没有必要打开,因此就保留了原样。为了方便起见,把邮箱SMTP功能配置上,这样就可以去验证邮箱使用邮箱验证登录,同时主密码忘记提示词也可以发送到邮箱。配置上DOMAIN,记得要带上前面的https和后面的端口,这样就可以使用webauth了。一切 就绪,就可以去重新启动docker了。

1
2
3
docker stop vaultwarden
docker rm vaultwarden
docker run -d --name vaultwarden -v /data/vw-data:/data --network host --env-file /env --restart unless-stopped  ghcr.io/dani-garcia/vaultwarden:latest

虽然把之前的docker容器删掉了,但是因为数据是映射到本地目录的,所以都还在。

前面这些搞完之后,为了数据的安全,我们还需要定期对数据进行备份,我是把阿里云盘挂载到本地了,因此直接把数据文件拷贝过去就实现了远程备份。为了足够高的安全,我是备份了两份,一份在阿里云盘,一份放到家里的另一块硬盘上。具体通过crontab每天定时执行脚本,把数据目录压缩,放置到对应的目录,备份的时候会把最老的那一个备份删掉。脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/bin/bash

SRC_DIR=/mnt/sda1/vw-data/
LOG_FILE=/mnt/sda1/log/Error_Log_$(date +%Y%m%d).log
MAX_NUM=10
NOTIFY_URL=https://sctapi.ftqq.com/[apikey].send
DEST_ALIYUN=/mnt/aliyundriver/backup/vaultwarden
DEST_SDB=/mnt/sdb1/backup/vaultwarden


function notify {
	curl --data-urlencode "title=${1}" "${NOTIFY_URL}"
}


function log {
	echo "$(date +'%Y-%m-%d %H:%M:%S') $1" >> ${LOG_FILE}
	notify "$1"
	
}

function compress {
	if [ ! -d "${DEST_SDB}" ]; then
		log "错误:第二块硬盘不存在,无法进行备份"
		return
	fi
	dest_file=${DEST_SDB}/vw_backup_$(date +%Y%m%d).zip
	7z a -tzip ${dest_file} ${SRC_DIR} > /dev/null 2>&1
	if [ $? -eq 0 ]; then
		echo "压缩完成,文件存在${dest_file}"
		if [ ! -d "${DEST_ALIYUN}" ];then
			log "错误:阿里云目录未挂载,请检查"
			return
		fi
		cp ${dest_file} ${DEST_ALIYUN}/
	else
		log "错误:压缩出现错误"
		return
	fi
	notify "今日备份成功$(date +%Y%m%d)"
}

function delete_old_archives {
	num=$(ls -l ${1} | grep "^-" | wc -l) 
	echo $num
	while  [ ${num} -gt ${MAX_NUM} ]
	do
		file=$(ls -rt ${1}/vw_backup_*.zip | head -n 1)
		if [ -n "${file}" ];then
			rm -f "${file}"
			echo "删除旧文件${file}"
		else
			echo "没有找到旧文件"
			break
		fi
		let num--
	done
}

function main {
	compress
	delete_old_archives "${DEST_ALIYUN}"
	delete_old_archives "${DEST_SDB}"
}

main

这样一通操作下来,自认为安全方面是有保障了,只是比之前的全部本地稍微差一点点。vaultwarden也提供了比较全的导入导出功能,因此我原来的keepass数据可以很容易到导入,基本做到了无缝切换。使用了两天下来,网页端的自动填充功能确实要强大很多。同时内置了OTP功能,一些需要二次验证的服务,可以自动把OTP Code输入了,这个后面可以把原来用的一些转移过来。唯独的问题是,vaultwarden的OTP code无法放到vaultwarden中去。

本来打算这个服务搭好之后,让老婆也一起用,提高全家账号的安全性,然而她却说用不上,手机上基本不需要输入密码,没必要多记一个密码了。不过对于大部分人来说也确实是,短信登录加上微信登录已经解决了大部分场景,密码管理服务对他们来说只是伪需求。也可能只对于我们这一小部分爱折腾的人才比较有点用吧。

搭建这个服务,参考了不少网上的内容。关于docker的使用和shell脚本的编写,也多亏了GPT。最后在列出一些参考了的资料:

  1. Vaultwarden wiki 中文版
  2. 自建 vaultwarden / bitwarden_rs 密码管理器
  3. Lucky使用指南

看完评论一下吧

Android源码分析: 应用启动安装ContentProvider分析

2024-08-15 20:02:56

ContentProvider是Android应用开发的四大组件之一,并且源码相对于其他几个也是比较简单的。因此我们先来看看它的源码。ContentProvider的使用我们会涉及到外部程序调用应用的ContentProvider来查询数据,也有监听数据的变化,以及ContentProvider的安装。我们先来看安装部分的源码。

ContentProvider根据它的名字就知道,他是一个内容提供者,它提供了整删改查的接口,方便Android应用跨应用跨进程的数据共享,在Android系统中,相册,通讯录等等都是通过ContentProvider来共享数据让其他应用可以使用。看源码,我们需要关注两个点,一个是ContentProvider如何安装的,另一个就是当我们发起一个查询的时候,是怎样和内容提供的那个进程进行交互的。

ContentProvider的安装触发通常有两个场景,一是外部程序需要使用ContentProvider的时候,另一个是在应用Application启动的时候,这个一般触发的场景有,启动Service,启动Activity。这所有场景的共同点都是拉起进程,初始化Application。这两大场景来安装ContentProvider除了开始的路径会有差别,后面的部分大致都相等。因此我们这里以启动App同时安装Provider作为分析路径。

ContentProvider相关的类有如下这些:

classDiagram
class IContentProvider
<<interface>> IContentProvider
namespace SYSTEM_SERVER进程 {
class ContentProviderRecord
class ProviderMap
class ProcessProviderRecord
}

namespace 应用进程 {
class ContentProvider
class ContentProviderNative
class Transport
class ProviderClientRecord
class Binder
}
<<abstract>> ContentProvider
class ProviderInfo
<<abstract>> ContentProviderNative
ContentProviderRecord *-- IContentProvider
ContentProviderRecord *-- ProviderInfo
IContentProvider <|.. ContentProviderNative
Binder <|-- ContentProviderNative
ContentProviderNative <|-- Transport
ContentProvider *-- Transport
class ContentProviderHolder
ContentProviderHolder *-- IContentProvider
ProviderClientRecord *-- IContentProvider
ProviderClientRecord *-- ContentProvider
ProviderClientRecord *-- ContentProviderHolder
ContentProviderHolder *-- ProviderInfo
ProviderMap o-- ContentProviderRecord
ProcessProviderRecord o-- ContentProviderRecord

PackageManager解析出来的Provider信息通过ProviderInfo来保存,我们平时创建的ContentProvider它有一个内部类Transport,在它里面实现了Binder的客户端和服务端。通过它的Binder代理,AMS进程能够执行ContentProvider的增删改查,这不是本文的重点,下次再说。在App进程内,除了我们的ContentProvider对象,还会构建ProviderClientRecord对象,ContentProvider和它的binder对象和ProviderInfo信息会存在这个对象中。在服务端也有一个对应的ContentProviderRecord对象,里面存储了IcontentProvider binder对象和ProviderInfo。并且客户端和服务端的这些record都会存到Map中去,这样方便后续调用的时候查找。而构建所有这些record和把他们放入到Map中的过程,其实就是Provider的安装过程。

我们开始看代码,在应用进程创建完成之后,AMS会执行attachApplicationLocked从而来创建App的Application对象,因此我们从这里开始看,而进程的启动可以挖个坑以后再写。最初起点应该在ActivityManagerServiceattachApplicationLocked方法中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);  
List<ProviderInfo> providers = normalMode  
                                    ? mCpHelper.generateApplicationProvidersLocked(app)  
                                    : null;
...
final ProviderInfoList providerList = ProviderInfoList.fromList(providers);
thread.bindApplication(processName, appInfo,  
        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,  
        providerList, null, profilerInfo, null, null, null, testMode,  
        mBinderTransactionTrackingEnabled, enableTrackAllocation,  
        isRestrictedBackupMode || !normalMode, app.isPersistent(),  
        new Configuration(app.getWindowProcessController().getConfiguration()),  
        app.getCompat(), getCommonServicesLocked(app.isolated),  
        mCoreSettingsObserver.getCoreSettingsLocked(),  
        buildSerial, autofillOptions, contentCaptureOptions,  
        app.getDisabledCompatChanges(), serializedSystemFontMap,  
        app.getStartElapsedTime(), app.getStartUptime());

可以看到AMS中首先是通过mCpHelper去生成当前应用的Provider列表,之后调用应用进程的bindApplication的时候再带过去。mCpHelper是一个ContentProviderHelper对象,我们先来看看它的generateApplicationProvidersLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
List<ProviderInfo> generateApplicationProvidersLocked(ProcessRecord app) {
	final List<ProviderInfo> providers;
	providers = AppGlobals.getPackageManager().queryContentProviders(  
                    app.processName, app.uid, ActivityManagerService.STOCK_PM_FLAGS  
                        | PackageManager.GET_URI_PERMISSION_PATTERNS  
                        | PackageManager.MATCH_DIRECT_BOOT_AUTO, /*metaDataKey=*/ null)  
                .getList();
    int numProviders = providers.size();  
	final ProcessProviderRecord pr = app.mProviders;  
	pr.ensureProviderCapacity(numProviders + pr.numberOfProviders());
	for (int i = 0; i < numProviders; i++) {  
	    // NOTE: keep logic in sync with installEncryptionUnawareProviders  
	    ProviderInfo cpi = providers.get(i);  
	    boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,  
            cpi.name, cpi.flags);  
	    if (singleton && app.userId != UserHandle.USER_SYSTEM) {  
	        // This is a singleton provider, but a user besides the  
	        // default user is asking to initialize a process it runs        
	        // in...  well, no, it doesn't actually run in this process,        // it runs in the process of the default user.  Get rid of it.        
		    providers.remove(i);  
	        numProviders--;  
	        i--;  
	        continue;  
	    }  
	    final boolean isInstantApp = cpi.applicationInfo.isInstantApp();  
	    final boolean splitInstalled = cpi.splitName == null || ArrayUtils.contains(  
            cpi.applicationInfo.splitNames, cpi.splitName);  
	    if (isInstantApp && !splitInstalled) {  
	        // For instant app, allow provider that is defined in the provided split apk.  
		    // Skipping it if the split apk is not installed.       
		    providers.remove(i);  
		    numProviders--;  
	        i--;  
	        continue;  
	    }  
  
	    ComponentName comp = new ComponentName(cpi.packageName, cpi.name);  
	    ContentProviderRecord cpr = mProviderMap.getProviderByClass(comp, app.userId);  
	    if (cpr == null) {  
	        cpr = new ContentProviderRecord(mService, cpi, app.info, comp, singleton);  
	        mProviderMap.putProviderByClass(comp, cpr);  
	    }  
	    pr.installProvider(cpi.name, cpr);  
	    if (!cpi.multiprocess || !"android".equals(cpi.packageName)) {   
	        app.addPackage(cpi.applicationInfo.packageName, cpi.applicationInfo.longVersionCode,  
                mService.mProcessStats);  
	    }  
	    mService.notifyPackageUse(cpi.applicationInfo.packageName,  
            PackageManager.NOTIFY_PACKAGE_USE_CONTENT_PROVIDER);  
	}  
	return providers.isEmpty() ? null : providers;
}

上面第三行代码为调用PackageManagerService去读取当前应用所有的ContentProvider信息并存储到ProviderInfo列表中,具体代码在ComputerEngine中,这里不分析了。ProviderInfo中存储了每一个ContentProvider的信息,包括它的组件名称,查询的authority,运行的进程,读写的权限等等。这里我们需要注意一下,我们在Manifest文件中声明ContentProvider的时候,是可以指定它所运行的进程的,在这个地方,我们传进来而的app也是一个ProcessRecord进程,它对应的是我们的一个进程的记录而不是app的记录,因此,我们拿到的ProviderInfo也是当前进程需要启动的所有进程。

随后会开启一个循环对每一个Provider做处理,在37行,通过packagename和name组合出ComponentName,这个和其他的构造Activity,Service等的类似。随后会尝试从ProviderMap中获取已经存在的记录,正常情况下这里都是空,如果一个App有多个进程,并且provider可以在多个进程运行,那么这里可能是可以拿到缓存的。

如果没有拿到缓存,我们会开始创建ContentProviderRecord,这是ContentProvider在AMS当中的记录,而它也会放到ProviderMap中,这样下次使用的时候就不需要再次创建了。

43行会调用ProcessProviderRecord的installProvider,这里只是把这条record存放到ProcessRecord的mProviders中去。

我们再回到AMS的代码中去,AMS当中会把我们的List<ProviderInfo>包装成一个ProviderInfoList对象,最后调用到ApplicationThread的bindApplication方法,从而把这些东西传递到App进程。

来到ActivityThread的源码,bindApplication会把AMS带过来的数据封装成AppBindData,通过sendMessage把传递ActivityThread类,并且调用它的handleBindApplication方法,其中我们会看到如下代码:

1
2
3
4
5
6
7
8
9
Application app;
app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
if (!data.restrictedBackupMode) {  
    if (!ArrayUtils.isEmpty(data.providers)) {  
        installContentProviders(app, data.providers);  
    }  
}

mInstrumentation.callApplicationOnCreate(app);

上面第5行就是去安装ContentProviders。第二行是去创建我们的Application,其中会调用Application的attach方法,第9行是调用Application的onCreate方法,可见ContentProvider的一些方法是在他们两之间执行的,这也是为什么很多SDK通过使用ContentProvider来初始化他们的代码。继续看installContentProviders的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final ArrayList<ContentProviderHolder> results = new ArrayList<>();
for (ProviderInfo cpi : providers) {
	ContentProviderHolder cph = installProvider(context, null, cpi,  
        false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
    if (cph != null) {  
    cph.noReleaseNeeded = true;  
    results.add(cph);  
}
}
ActivityManager.getService().publishContentProviders(  
    getApplicationThread(), results);

上面的代码就是去遍历每一个ContentProvider去安装,我们继续看installProvider的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ContentProvider localProvider = null;
IContentProvider provider;
if (holder == null || holder.provider == null) {
	Context c = null;  
	ApplicationInfo ai = info.applicationInfo;  
	if (context.getPackageName().equals(ai.packageName)) {  
	    c = context;  
	}
	...
	final java.lang.ClassLoader cl = c.getClassLoader();  
	LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);  
	if (packageInfo == null) {  
	    // System startup case.  
	    packageInfo = getSystemContext().mPackageInfo;  
	}  
	localProvider = packageInfo.getAppFactory()  
        .instantiateProvider(cl, info.name);  
	provider = localProvider.getIContentProvider();
	
	localProvider.attachInfo(c, info);
}
...
synchronized (mProviderMap) {
	IBinder jBinder = provider.asBinder();
	if (localProvider != null) {
		ComponentName cname = new ComponentName(info.packageName, info.name);  
	
		holder = new ContentProviderHolder(info);  
		holder.provider = provider;  
		holder.noReleaseNeeded = true;  
		pr = installProviderAuthoritiesLocked(provider, localProvider, holder);  
		mLocalProviders.put(jBinder, pr);  
		mLocalProvidersByName.put(cname, pr);

	}
	
	...
}

以上代码简化很多,仅保留启动App安装Provider的代码。第10行到第17行的代码,为通过LoadedApk通过反射去创建ContentProvider这个对象,随后通过它拿到IContentProvider对象,也就是它的Binder对象。随后调用attachInfo方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private void attachInfo(Context context, ProviderInfo info, boolean testing) {
	mCallingAttributionSource = new ThreadLocal<>();
	mContext = context;  
	if (context != null && mTransport != null) {  
	    mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService(  
            Context.APP_OPS_SERVICE);  
	}  
	mMyUid = Process.myUid();  
	if (info != null) {  
	    setReadPermission(info.readPermission);  
	    setWritePermission(info.writePermission);  
	    setPathPermissions(info.pathPermissions);  
	    mExported = info.exported;  
	    mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;  
	    setAuthorities(info.authority);  
	}  

	ContentProvider.this.onCreate();
}

可以看到其中是为ContentProvider设置一些信息,包括它的Context,以及把Manifest上面设置的一些属性,权限之类的保存到当前这个对象中,最后会调用onCreate方法,这会执行我们重写时写的代码。

再回到installProvider方法,在28行,会创建ContentProviderHolder,随后调用installProviderAuthoritiesLocked把Provider和它所对应的authority对应,并创建ProviderClientRecord,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,  
        ContentProvider localProvider, ContentProviderHolder holder) {
	final String auths[] = holder.info.authority.split(";");  
	final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);
	...
	final ProviderClientRecord pcr = new ProviderClientRecord(  
        auths, provider, localProvider, holder);
    for (String auth : auths) {  
	    final ProviderKey key = new ProviderKey(auth, userId);  
	    final ProviderClientRecord existing = mProviderMap.get(key);
	    if (existing != null) {  
		} else {  
		    mProviderMap.put(key, pcr);  
		}
	}
	return pcr;
}

上面的代码很简单,就是创建了ProviderClientRecord,其中保存了auths,我们创建的ContentProvider,以及IContentProvider,ContentProviderHolder,最后把每个authority作为key, ProviderClientRecord作为value,存放到了mProviderMap中。

上面的代码执行完之后,在installProvider中,又分别以binder对象和ComponentName对象为key,ProviderClientRecord对象为value存放到map中。

这一切做完之后,我们还需要回到installContentProviders方法的最后,看看第10行的代码,看代码名称是发布我们的Provider,那我们继续到AMS中去看代码,其中主要调用了如下代码:

1
mCpHelper.publishContentProviders(caller, providers);

继续去ContentHelper中看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void publishContentProviders(IApplicationThread caller, List<ContentProviderHolder> providers) {
	synchronized (mService) {
		final ProcessRecord r = mService.getRecordForAppLOSP(caller);
		for (int i = 0, size = providers.size(); i < size; i++) {
			ContentProviderHolder src = providers.get(i);
			ContentProviderRecord dst = r.mProviders.getProvider(src.info.name);
			if (dst == null) {  
			    continue;  
			}
			ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);  
			mProviderMap.putProviderByClass(comp, dst);
			String[] names = dst.info.authority.split(";");  
			for (int j = 0; j < names.length; j++) {  
			    mProviderMap.putProviderByName(names[j], dst);  
			}
			r.addPackage(dst.info.applicationInfo.packageName,  
	        dst.info.applicationInfo.longVersionCode, mService.mProcessStats);
	        synchronized (dst) {  
			    dst.provider = src.provider;  
			    dst.setProcess(r);  
			    dst.notifyAll();  
			    dst.onProviderPublishStatusLocked(true);  
			}
		}
	}      
}

上面的代码第3行,通过我们传过来的IApplicationThread来获取到我们的进程在AMS当中对应的ProcessRecord。随后会遍历每一个ContentProviderHolder,检查ProcessRecord当中的Record是否都有,随后会把ProcessRecord当中所存储的的CotentProviderRecord按照类名和authority分别存储到mProviderMap当中,ContentProviderHolder会存储到ContentProviderRecord当中。

最后也放一下整个流程的流程图方便看代码:

sequenceDiagram
autonumber
box LIGHTYELLOW SYSTEM_SERVER进程
participant AMS
participant ContentProviderHelper
participant PMS
participant ProcessProviderRecord
end
box LIGHTGREEN 应用进程
participant ApplicationThread
participant ActivityThread
participant ContentProvider
end
rect rgb(191, 223, 255)
note right of AMS: attachApplicationLocked
AMS->>+ContentProviderHelper: generateApplicationProvidersLocked
ContentProviderHelper->>+PMS: queryContentProviders
PMS-->>-ContentProviderHelper: List<ProviderInfo>
ContentProviderHelper->>ProcessProviderRecord: installProvider
ContentProviderHelper-->>-AMS: List<ProviderInfo>
AMS->>ApplicationThread: bindApplication(binder call)
end
ApplicationThread->>ActivityThread: handleBindApplication
rect rgb(191, 223, 255)
note right of ActivityThread: installContentProviders
ActivityThread->>ActivityThread: installProvider
ActivityThread->>ContentProvider: attachInfo
ActivityThread->>ActivityThread: installProviderAuthoritiesLocked
end

ActivityThread->>AMS: publishContentProviders
AMS->>ContentProviderHelper: publishContentProviders

至此,就执行完了所有的ContentProvider安装的工作。至于使用ContentProvider的场景,我们之后在继续分析。本文以Android13的代码分析,如果读者对照最好也是以同样版本的代码看。以上是本人关于Android代码阅读的一点分享,由于个人可能存在一些误区,难免会有理解错误,或者笔误,如有发现,欢迎指正,也欢迎读者与我交流Android技术。

(文中类图,时序图使用mermaid绘制,如果使用rss无法渲染,请点击原文查看)

看完评论一下吧

Android源码分析:ClientTransaction分析

2024-08-13 21:59:04

分析Android Activity的启动过程,发现Android 在Android9.0中引入了ClientTransaction这一系列的对象,来简化system_server与App进程中处理Activity启动相关的任务。这里就来分析一下。

在服务端(system_server进程)主要有上面这些类,我们首先需要关注的就是ClientTransaction类,这个类在使用的时候主要是有以下几个成员:

1
2
3
4
private List<ClientTransactionItem> mActivityCallbacks;
private ActivityLifecycleItem mLifecycleStateRequest;
private IApplicationThread mClient;
private IBinder mActivityToken;

mClient是对应的app的ApplicationThread,他是一个Binder对象,mActivityToken则是Activity的Binder Token,这两个在很多地方都会看到。而mActivityCallbacks为ClientTransactionItem对象,比如说LaunchActivityItemNewIntentItem这些都是它的子类,同一个ClientTransactionItem中是可以有多个的。mLifecycleStateRequestActivityLifecycleItem,它的子类是PauseActivityItemStopActivityItem这些,每一个是希望Activity执行到的一个状态。相关的类的类图如下:

classDiagram
class Parcelable
<<interface>> Parcelable
class BaseClientRequest
<<abstract>> BaseClientRequest
class ClientTransactionItem
<<abstract>> ClientTransactionItem
BaseClientRequest <|-- ClientTransactionItem
Parcelable <.. ClientTransactionItem
BaseClientRequest: void preExecute(ClientTransactionHandler client, IBinder token)
BaseClientRequest: void execute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions)
BaseClientRequest: void postExecute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions)
ClientTransactionItem: public int getPostExecutionState()
ClientTransactionItem: boolean shouldHaveDefinedPreExecutionState()
class ActivityTransactionItem
<<abstract>> ActivityTransactionItem
ClientTransactionItem <|-- ActivityTransactionItem
ActivityTransactionItem: ActivityClientRecord getActivityClientRecord(ClientTransactionHandler client, IBinder token)
ActivityTransactionItem <|-- LaunchActivityItem
ActivityTransactionItem <|-- NewIntentItem
ActivityTransactionItem <|-- ActivityResultItem
ActivityTransactionItem <|-- `..`
class ActivityLifecycleItem
ActivityTransactionItem <|-- ActivityLifecycleItem
ActivityLifecycleItem: public abstract int getTargetState()
ActivityLifecycleItem <|-- ResumeActivityItem
ActivityLifecycleItem <|-- PauseActivityItem
ActivityLifecycleItem <|-- StartActivityItem
ActivityLifecycleItem <|-- StopActivityItem
ActivityLifecycleItem <|-- DestroyActivityItem

启动Activity时候的代码调用在ActivityTaskSupervisor这个类的realStartActivityLocked方法中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
final ClientTransaction clientTransaction = ClientTransaction.obtain(  
        proc.getThread(), r.token);
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),  
        System.identityHashCode(r), r.info,  
        // TODO: Have this take the merged configuration instead of separate global  
        // and override configs.  
        mergedConfiguration.getGlobalConfiguration(),  
        mergedConfiguration.getOverrideConfiguration(), r.compat,  
        r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,  
        proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),  
        results, newIntents, r.takeOptions(), isTransitionForward,  
        proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,  
        r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
final ActivityLifecycleItem lifecycleItem;  
if (andResume) {  
    lifecycleItem = ResumeActivityItem.obtain(isTransitionForward,  
            r.shouldSendCompatFakeFocus());  
} else {  
    lifecycleItem = PauseActivityItem.obtain();  
}  
clientTransaction.setLifecycleStateRequest(lifecycleItem);
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

从上面的代码我们看到ClientTransaction以及其他的一些使用到的比较多的对象,Android系统中都做了对象池,内部基本上都是数组维护,我们这里不分析了。

启动Activity就是创建了一个LaunchActivityItem并且设置了对应的LifecycleStateRequest,最后是通过LifecycleManager调用scheduleTransaction来执行。这里的mServiceActivityTaskManagerService的实例,lifecycleManagerClientLifecycleManager的实例,方法代码如下:

1
2
3
4
5
6
7
void scheduleTransaction(ClientTransaction transaction) throws RemoteException {  
    final IApplicationThread client = transaction.getClient();  
    transaction.schedule();  
    if (!(client instanceof Binder)) {  
         transaction.recycle(); 
    }  
}

这里ClientTransaction会获取Client,也就是IApplicationThread,system_server这一端是ApplicationThread的客户端,因此schedule和recycle方法都会执行。主要看一下schedule方法:

1
2
3
public void schedule() throws RemoteException {  
    mClient.scheduleTransaction(this);  
}

又调用了Client的scheduleTransaction方法,参数为我们的ClientTransaction方法,这是在客户端调用,那我们需要去服务端看这个方法的执行,实现也就是在ApplicationThread类当中。

1
2
3
public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {  
    ActivityThread.this.scheduleTransaction(transaction);  
}

ActivityThreadClientTransactionHandler的子类,这个scheduleTransaction就在ClientTransaction当中,代码如下:

1
2
3
4
5

void scheduleTransaction(ClientTransaction transaction) {  
    transaction.preExecute(this);  
    sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);  
}

其中的preExecute方法就是直接调用了,内部实现就是分别调用所有的activityCallbackpreExecute方法,以及mLifecycleStateRequestpreExecute方法,而execute没有直接调用,而是通过消息发出去了,实现就是ActivityThread的sendMessage,我们也都知道是用它的H这个Handler,可以在它的handleMessage中找到如下代码:

1
2
3
4
5
6
case EXECUTE_TRANSACTION:  
    final ClientTransaction transaction = (ClientTransaction) msg.obj;  
    mTransactionExecutor.execute(transaction);  
    if (isSystem()) {  
        transaction.recycle();  
    }

可以看到这里是通过TransactionExecutor来调用execute,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public void execute(ClientTransaction transaction) {  
    final IBinder token = transaction.getActivityToken();  
    if (token != null) {  
        final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed =  
                mTransactionHandler.getActivitiesToBeDestroyed();  
        final ClientTransactionItem destroyItem = activitiesToBeDestroyed.get(token);  
        if (destroyItem != null) {  
            if (transaction.getLifecycleStateRequest() == destroyItem) {  
                 activitiesToBeDestroyed.remove(token);  
            }  
            if (mTransactionHandler.getActivityClient(token) == null) {  
                return;  
            }  
        }  
    } 
  
    executeCallbacks(transaction);  
    executeLifecycleState(transaction);  
    mPendingActions.clear();
}

public void executeCallbacks(ClientTransaction transaction) {  
    final List<ClientTransactionItem> callbacks = transaction.getCallbacks();  
    if (callbacks == null || callbacks.isEmpty()) {  
        return;  
    }  
  
    final IBinder token = transaction.getActivityToken();  
    ActivityClientRecord r = mTransactionHandler.getActivityClient(token);  
  
    final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();  
    final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()  
            : UNDEFINED;  
    final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);  
  
    final int size = callbacks.size();  
    for (int i = 0; i < size; ++i) {  
        final ClientTransactionItem item = callbacks.get(i);  
        final int postExecutionState = item.getPostExecutionState();  
  
        if (item.shouldHaveDefinedPreExecutionState()) {  
            final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,  
                    item.getPostExecutionState());  
            if (closestPreExecutionState != UNDEFINED) {  
                cycleToPath(r, closestPreExecutionState, transaction);  
            }  
        }  
  
        item.execute(mTransactionHandler, token, mPendingActions);  
        item.postExecute(mTransactionHandler, token, mPendingActions);  
        if (r == null) {  
            r = mTransactionHandler.getActivityClient(token);  
        }  
  
        if (postExecutionState != UNDEFINED && r != null) {  
            // Skip the very last transition and perform it by explicit state request instead.  
            final boolean shouldExcludeLastTransition =  
                    i == lastCallbackRequestingState && finalState == postExecutionState;  
            cycleToPath(r, postExecutionState, shouldExcludeLastTransition, transaction);  
        }  
    }  
}

execute中调用了executeCallbacks和executeLifecycleState这两个方法,我们上面贴出了前一个方法的代码,这里先来分析。 首先来介绍一下TransactionExecutor的一个成员变量mTransactionHandler我们可以发现TransactionExecutor的初始化是在ActivityThread中,这个的mTransactionHandler就是ActivityThread。 这里首先会先判断Activity是否需要执行到某一个状态,也就是通过getPostExecutionState来设置,比如NewItentItem中是有设置的,而LaunchActivityItem则不需要。如果Activity需要进入指定的状态,则会调用cycleToPath来执行到对应的状态,我们后面再分析。 随后就会调用item的execute方法和postExecute方法。我们就来看一下LaunchActivityItem的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void execute(ClientTransactionHandler client, IBinder token,  
        PendingTransactionActions pendingActions) {  
    Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");  
    ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,  
            mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,  
            mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,  
            client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,  
            mTaskFragmentToken);  
    client.handleLaunchActivity(r, pendingActions, null /* customIntent */);  
    Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);  
}

我们知道这里的client就是我们的ActivityThread,而这个handleLaunchActivity的实现也是在ActivityThread中实现,而启动Activity的参数都是在这个LaunchActivityItem里面的,他们通过Binder跨进程从system_server传到了app进程。这样一来原来放在ApplicationThread当中的handleLaunchActivity方法就抽离到LaunchActivity中了,ActivityThread这个文件中的很多代码就抽出去了。Activity启动的流程这里就不分析了,继续去看看后面的代码。

这里执行完了还是会检查是否需要把Activity推进到某一个状态,如果是LaunchActivityItem还是不需要的。我们继续去看executeLifecycleState的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void executeLifecycleState(ClientTransaction transaction) {  
    final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();  
    if (lifecycleItem == null) {  
        // No lifecycle request, return early.  
        return;  
    }  
  
    final IBinder token = transaction.getActivityToken();  
    final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);  
  
    if (r == null) {  
        // Ignore requests for non-existent client records for now.  
        return;  
    }  
  
    // Cycle to the state right before the final requested state.  
    cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */, transaction);  
  
    // Execute the final transition with proper parameters.  
    lifecycleItem.execute(mTransactionHandler, token, mPendingActions);  
    lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);  
}

可以看到其中还是首先调用了cycleToPath这个方法,但是我们需要注意这里这个方法的调用,excludeLastState这个值传的是true,也就是说如果我们设置的LifecycleStateResumeActivityItem那么它会把状态设置为ON_START而不是ON_RESUME。为什么这样做呢,因为后面还会调用lifecycleItem.execute,在其中我们会自行把状态推进到我们需要的状态,ResumeActivityItem的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void execute(ClientTransactionHandler client, ActivityClientRecord r,  
        PendingTransactionActions pendingActions) {  
    Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume");  
    client.handleResumeActivity(r, true /* finalStateRequest */, mIsForward,  
            mShouldSendCompatFakeFocus, "RESUME_ACTIVITY");  
    Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);  
}  
  
@Override  
public void postExecute(ClientTransactionHandler client, IBinder token,  
        PendingTransactionActions pendingActions) {  
    ActivityClient.getInstance().activityResumed(token, client.isHandleSplashScreenExit(token));  
}

上面的代码中可以看到execute中是调用的ActivityThreadhandleResumeActivity方法,从而让Activity执行resume并且进入ON_RESUME状态。 我们再来看一下cycleToPath方法:

1
2
3
4
5
6
private void cycleToPath(ActivityClientRecord r, int finish, boolean excludeLastState,  
        ClientTransaction transaction) {  
    final int start = r.getLifecycleState();  
    final IntArray path = mHelper.getLifecyclePath(start, finish, excludeLastState);  
    performLifecycleSequence(r, path, transaction);  
}

其中首先是拿到Activity当前的状态,再通过mHelper拿到我们需要执行的所有状态,代码如下(异常判断代码移除了,这里关注重点):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public IntArray getLifecyclePath(int start, int finish, boolean excludeLastState) {  

  
    mLifecycleSequence.clear();  
    if (finish >= start) {  
        if (start == ON_START && finish == ON_STOP) {  
            mLifecycleSequence.add(ON_STOP);  
        } else {  
            // just go there  
            for (int i = start + 1; i <= finish; i++) {  
                mLifecycleSequence.add(i);  
            }  
        }  
    } else { // finish < start, can't just cycle down  
        if (start == ON_PAUSE && finish == ON_RESUME) {  
            // Special case when we can just directly go to resumed state.  
            mLifecycleSequence.add(ON_RESUME);  
        } else if (start <= ON_STOP && finish >= ON_START) {  
            // Restart and go to required state.  
  
            // Go to stopped state first.            for (int i = start + 1; i <= ON_STOP; i++) {  
                mLifecycleSequence.add(i);  
            }  
            // Restart  
            mLifecycleSequence.add(ON_RESTART);  
            // Go to required state  
            for (int i = ON_START; i <= finish; i++) {  
                mLifecycleSequence.add(i);  
            }  
        } else {  
            // Relaunch and go to required state  
  
            // Go to destroyed state first.            for (int i = start + 1; i <= ON_DESTROY; i++) {  
                mLifecycleSequence.add(i);  
            }  
            // Go to required state  
            for (int i = ON_CREATE; i <= finish; i++) {  
                mLifecycleSequence.add(i);  
            }  
        }  
    }  
  
    // Remove last transition in case we want to perform it with some specific params.  
    if (excludeLastState && mLifecycleSequence.size() != 0) {  
        mLifecycleSequence.remove(mLifecycleSequence.size() - 1);  
    }  
  
    return mLifecycleSequence;  
}

可以看到以上代码还是比较简单的,就是按照顺序把Android Activity生命周期状态,按照当前的状态和需要执行结束的状态,把需要执行的放到数组中,最后再看看最后的那个状态要不要移除掉。我们继续看performLifecycleSequence的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void performLifecycleSequence(ActivityClientRecord r, IntArray path,  
        ClientTransaction transaction) {  
    final int size = path.size();  
    for (int i = 0, state; i < size; i++) {  
        state = path.get(i);  
        switch (state) {  
            case ON_CREATE:  
                mTransactionHandler.handleLaunchActivity(r, mPendingActions,  
                        null /* customIntent */);  
                break;  
            case ON_START:  
                mTransactionHandler.handleStartActivity(r, mPendingActions,  
                        null /* activityOptions */);  
                break;  
            case ON_RESUME:  
                mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */,  
                        r.isForward, false /* shouldSendCompatFakeFocus */,  
                        "LIFECYCLER_RESUME_ACTIVITY");  
                break;  
            case ON_PAUSE:  
                mTransactionHandler.handlePauseActivity(r, false /* finished */,  
                        false /* userLeaving */, 0 /* configChanges */,  
                        false /* autoEnteringPip */, mPendingActions,  
                        "LIFECYCLER_PAUSE_ACTIVITY");  
                break;  
            case ON_STOP:  
                mTransactionHandler.handleStopActivity(r, 0 /* configChanges */,  
                        mPendingActions, false /* finalStateRequest */,  
                        "LIFECYCLER_STOP_ACTIVITY");  
                break;  
            case ON_DESTROY:  
                mTransactionHandler.handleDestroyActivity(r, false /* finishing */,  
                        0 /* configChanges */, false /* getNonConfigInstance */,  
                        "performLifecycleSequence. cycling to:" + path.get(size - 1));  
                break;  
            case ON_RESTART:  
                mTransactionHandler.performRestartActivity(r, false /* start */);  
                break;  
            default:  
                throw new IllegalArgumentException("Unexpected lifecycle state: " + state);  
        }  
    }  
}

这里可以看到代码其实很简单,就是按照顺序调用mTransactionHandler也就是ActivityThread的各个生命周期需要执行的方法。

ActivityThread和TransactionExecutor的关系如下图:

classDiagram
class ActivityThread
class TransactionExecutor
class ClientTransactionHandler
<<abstract>> ClientTransactionHandler
ClientTransactionHandler <|-- ActivityThread
TransactionExecutor .. ClientTransactionHandler
ActivityThread *-- TransactionExecutor
ActivityThread: TransactionExecutor mTransactionExecutor
TransactionExecutor: ClientTransactionHandler mTransactionHandler
TransactionExecutor: execute(ClientTransaction transaction)

这里也画一下时序图方便看代码,发送端的代码比较简单,这里只画一下接收和执行端的图

sequenceDiagram

autonumber

ApplicationThread->>ActivityThread: scheduleTransaction

ActivityThread->>ClientTransaction: preExecute

ActivityThread->>ActivityThread: sendMessage:EXECUTE_TRANSACTION

ActivityThread->>TransactionExecutor: execute

TransactionExecutor->>TransactionExecutor: executeCallbacks

TransactionExecutor->>ClientTransaction: execute

TransactionExecutor->>ClientTransaction:postExecute

TransactionExecutor->>TransactionExecutor: executeLifecycleState

TransactionExecutor->>TransactionExecutor: cycleToPath

TransactionExecutor->>ActivityThread: handleToTargetStatus

上面的分析是system_server调用,当然App进程也是可以自己调用的,比如下面的代码:

1
2
3
4
5
6
private void scheduleResume(ActivityClientRecord r) {  
    final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token);  
    transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false,  
            /* shouldSendCompatFakeFocus */ false));  
    executeTransaction(transaction);  
}

可以看到,在app进程中就直接调用TransactionExecutor的execut去执行了。 以上就是ClientTransaction的全部分析了。我也是在分析Android Activity启动的过程发现这些代码的,Activity的启动代码量巨大,在Android 9的时候把一部分代码抽离到ClientTransaction中去,在Android 10以后启动的相当一部分代码抽离到ActivityTaskManagerService中去了,让代码逻辑更清晰一点,不过绕来绕去的看代码也是很麻烦。

因此,这里先试试水,把其中的一小部分也就是ClientTransaction相关的先拿出来写一写。从我的角度看,为什么要抽出来ClientTransaction这个机制,原先所有的代码都需要在ApplicationThread中定义binder接口,而android随着各种屏幕的出现,小窗模式等等,这样的化每次有新功能都需要增加新的binder方法,而用了ClientTransaction则都可以通过scheduleTransaction来调用,同时ActivityThread内的代码也会有所减少,代码功能更加独立。

Android系统因为功能的代码,代码也更加复杂,很多地方因为许多的新功能多了很多逻辑判断,为我们看代码增加了难度。但是只要我们记住我们看的主线,关注我们一路传过来的值,关注我们自己会执行的那个分支,这样一路下来就可以把整个逻辑理清楚。

本文以Android13的代码分析,如果读者对照最好也是以同样版本的代码看。以上是本人关于Android代码阅读的一点分享,由于个人可能存在一些误区,难免会有理解错误,或者笔误,如有发现,欢迎指正,也欢迎读者与我交流Android技术。

(文中类图,时序图使用mermaid绘制,如果使用rss无法渲染,请点击原文查看)

看完评论一下吧

博客主题装修更新记录

2024-08-06 21:20:53

现在版本的博客是三年前搭建hugo程序的时候选择的hello-friends主题,用着没啥问题,但是一直想要做点改变,最近就动手干起来了。新主题基于PaperMod,更多更新如下。

首先换了个主题,主要是因为之前的主题用到了yarn编译,每次换电脑都要重新装一下node,npm,主题好久没有修改了,新装npm的时候居然给我报错,索性就直接换一个theme。挑选了一下PaperMod比较符合我的胃口,这个theme比较简洁,没有使用第三方的js,css预编译工具,使用了hugo去做js和css的预编译。

为了与众不同,这次为我这个修改版本的主题起了个名字“Zen“。主题的基础框架来自PaperMode,但是文章列表格式仍然复用了之前主题的格式,并且对于css进行了修改。之前的博客主题色是一个比较鲜艳的红色,这次换成了低调的黑色。另外页面的亮色主题参考了好多的网友的颜色,挑了一个接近书本的米黄色。

博客原有的friends,bilbil shortcodes是之前的时候从林木木博客那里学来的,这次仍旧拿过来改改css继续用。

paperMode是支持在首页添加一个个人简洁的,就顺手给加上了。除此之外,还把关于我的页面进行更新,对我自己进行了更加详细的介绍,欢迎阅读。

之前看到过很多博主都有做过足迹地图的功能,看起来很炫酷,我也一直想要做,一是老是犯懒,二是不知道从何下手。正好最近看到水八口首页的地图功能,去看了一下她的代码,使用的leaflet还不算复杂,就趁着这次博客大翻修,把足迹功能加上了。翻阅了hugo的文档实现了指定页面加载足迹地图,现在首页和行摄页面都把足迹地图给加上了。

地图底图使用的是cartocdn,跟水口八一样的简洁地图,同时把地图标记点放在了js文件中,页面上通过js去读取标记点添加到地图上,通过在自己的css中做修改来改动标记popup的样式并对其适配了夜间模式。对这个感兴趣的可以到github看我的源码。现在功能可以使用,唯独就是地点标记需要手动添加,后面有空可以再研究一下能够通过文章中添加属性,从而自动生成。

之前的代码渲染使用的是prism.js,这次修改博客,还是换成了使用hugo自己的代码渲染,希望能够提高博客的性能。

PaperMode内置了使用fuse实现的搜索功能,这次也给小站加上了。主要做的如下,config设置中要生成文章的json文件,修改如下:

1
2
[outputs]
  home = ["HTML","RSS","JSON"]

content中添加search文件夹,其中添加一个index.md文件。

1
2
3
4
5
6
7
8
9
---

title: "搜索"

url: [search]

layout: search

---

一定要添加这个layout:search,不然加载js的判断会读取不到layout属性,从而无法加载js,使得整个功能无法使用。

博客的RSS目前是全文输出的,这次给输出的RSS底部添加了博客的链接,能够点击直达博文评论去,欢迎大家评论。

因为本人不是前端开发,CSS的了解也只是一知半解,在主题定制过程中,多亏了GPT,让我能够在很多地方从容的实现想要的功能。

最后要说一下,我觉得博客的内容是最重要的,主题样式是次要的,后面还要继续努力好好写文章,也欢迎网友交流讨论。

看完评论一下吧

莫干山避暑玩水之旅

2024-08-05 19:54:15

最近天气一直比较热,几天之前朋友就约了一起周末去莫干山鱼鳞坝玩水。周末早上便一起驾车前往,开启两天的玩水之旅。

第一天上午驾车前往,大约中午十二点钟到达四合村,猜想鱼鳞坝应该会有很多人,便在这里停下来的,花了十块钱把车停到村民家里。随后便拿出天幕,露营椅,在水边搭好方便遮阳。因为也没有带泳衣,我和小朋友一起都脱的只剩内裤下水游玩,同行的家属则在天幕下歇息。

这里的水深度还可以,有适合小朋友游玩的浅水,也有适合大人游泳的深水区,就是水面上没有树荫比较热,另外水中水草较多,水不太清澈。但是这一切都不能阻挡小朋友们玩水的热情。

在附近想要找到吃饭的店也没有,便在河边的小店花了30多元买了几个山寨可爱多和山寨老冰棍分给大家吃。刚吃完冰棍没多久,就有村里的大妈过来声称这个浅滩是她搭天幕的,我们需要给她点好处费,具体多少也没说,旁边的天幕给了几个圣女果打发了,而我气不过,决定收掉天幕换个地方。大妈刚走,天幕还没收好,当地的政府的人员就过来了,告知现在在保护水域,禁止搭建天幕,旁边的天幕也就一起收掉了。

收完天幕,决定先去找个地方吃个饭再继续玩。于是驾车前往溪北村,因为前往的路在修,便绕道平阳岭路,练了一把180度的大转弯。找了个农家乐随便吃了一点。之后便准备驾车前往水的源头去找个有树荫的地方继续玩水,沿着晓于线往上游走,然而因为路比较窄,旁边村民的停车场20一次,大家都把车停在路边,因此刚走了几百米就堵住了,等了一会仍不见缓解,家属和小朋友们就下车到旁边的小溪中玩耍了。 我与朋友继续开车前行,等到不再堵车,前面也未找到停车场,我们放便把车停在路边一处较为宽敞处。下车之后我们一起向前步行寻找停车场,到达接近福水村的位置处发现这里也是很堵,便决定往回走。

在路边看到有人在水里搭建天幕,这里挂有牌子,有村民明码标价停车20元,搭建天幕20元,然而这边的水感觉有点怪味道,似乎还没有下游的更干净。

走回到小朋友们玩水的地方,这里一个水坝把水分成两边,一边是有一定深度,小朋友在里面游泳正好,另一边这是各种石头,水倒是没有多少,可以踩着玩。

大约五点多钟,感觉小朋友们也都玩累了,便决定去县城。去的路上准备先去看看莫干山庾村广场,各种网上的攻略都会推荐这个地方。绕道过去, 进入庾村可以看到这边的民国街,老车站,许多有特点的商店和饮品店,停车在P4停车场,出来便是庾村广场,广场上一座民国风格钟楼,许多人在喷泉处嬉闹。稍微转转,便驾车前往德清县城,刚到酒店就下起大雨,这一天差不多就这样结束。

庾村广场

庾村广场

第二天一早就收到红色高温警报,便准备前往下渚湖湿地公园,刚刚提议完,小朋友们表示不愿意,都说还要去玩水游泳。而附近除了前一日游玩的小溪一线也没其他好地方了,便决定去鱼鳞坝试试运气。

然而在前往鱼鳞坝的过程中边发现前面堵车,最后在鱼鳞坝和龙鳞坝中间的一个地方停车下来,让小朋友们下水游玩。

朋友不死心,还是跑到鱼鳞坝去看了看,果然是,这么热的天仍然很多人。我们就在这里继续玩水一直到中午,随后送亲戚去湖州坐车会老家了。 送完人,我们其余几人在附近找了个外婆家好好吃了个饭,下午冒雨返回返回上海。

回上海的第二天,两侧肩膀均因晒伤发红痒痛。

看完评论一下吧

7月小记-可能是破财消灾吧

2024-07-31 17:44:41

七月即将结束,就在动笔前,一次爆胎为这个月定了调。这个月梅雨刚走,又来高温,除了上半个月出去玩了一趟,后面基本就在家待着,靠空调续命。

游玩

月初带着老婆小孩到山东玩了一趟,沿着沈海公路北上,经过日照,青岛,威海,烟台,一路上看海玩水,大人小孩都开心,山东半岛的海鲜价格也还比较实惠,吃的也很爽。具体可以看之前的博客文章👈

回上海之后,就开始一直高温,便一直待在家里。周末去了一次浦东的金海湿地公园,去时阴天,到了却是大晴天,热的全身出汗,待了一会便大道回家了。

车损

这个月可谓是运势不佳,月初去山东玩,在海边倒车撞上了渔船的螺旋桨,因为报案问题,保险又不全赔,4s店给解决了,好奇的可以看之前的文章。

今天,就在刚刚,送家人去地铁站,出门时候自信的一把出车位,结果轮胎被边上花坛的水泥围栏给刮破了,换个胎又要2000元。回想起去年刚提车没多久,就在小区因为边上的围栏把轮胎给弄坏了,这又来一次,真是伤心。

一年干坏两个胎,老婆听了直接建议我把车给卖掉了。当初没买车的时候想买车,买车了之后又遇到各种问题,小区里面停车位紧张,车位不好停,经常因此而避免晚上出门,或者近距离就选择了其他的出行方式了。再加上车技不佳,经常倒车就剐蹭到了,如果不是贴了车衣,可能车漆也都不能看了。当然买车也有好处,出去玩可以做到说走就走,安排更灵活。但究竟利大于弊还是弊大于利,却是很难说。

折腾

为了看Android的源码,再一次给电脑上装上了WIndows&Ubuntu的双系统。缘由是为了编译Android系统,起初打算在windows下面使用虚拟机,但是虚拟机挂载虚拟硬盘出现了问题,最后索性还是装个双系统。之前装双系统是和windows放在同一块硬盘上的,这样有个问题,占用1T的总空间不够两个系统用的,如果linux不想要了,windows很难恢复。 这次就选择拿了一块移动硬盘来安装ubuntu,这样windows的硬盘上面有windows的efi引导分区,移动硬盘上有ubuntu的引导分区,只需要在开机的时候选择启动分区就可以进入windows或者ubuntu了,移动硬盘拔掉也不影响windows的使用。

装完系统,把android的编译环境搞好,开始编译系统。顺便把Edge浏览器装上去,obsidian,Fcitx5,等工具也都装上,这样搞下来,除了不能使用微信聊天,不能玩游戏,在linux下面其他的事情也都可以很顺畅的完成。要说缺点,也是有的,刚刚开机的时候还是有点卡的,有些应用使用比windows下面也要卡一点,不知道是因为intel大小核的问题还是因为这个移动硬盘不是固态硬盘。

另外就是最主要的事情了,编译Android系统,花了几天的时间也终于把系统搞好,模拟器运行的镜像编译,以及导入到Android studio阅读源码。

月初出去玩,打算的很好,晚上的时候可以看看书或者做一些工作,然后一天玩下来还是挺累的,就没怎么看书。后面的时间,就看了两本书,以及《读库2403》的一篇文章。

第一本书是之前看过的电视剧《人生复本》的原著,电视剧我没看完,书的内容相比电视剧更加的紧凑,书的内容也挺精彩的。我的感觉是,电视剧把书的精华的东西都拿过去了,同时电视剧又对剧情增加了很多的扩充,可能这样方便电视剧能够拍更多季,期待后面新的电视剧的剧集。

第二本书是《夜航西飞》,这本书之前是池建强推荐过的,已经在我的微信读书书架躺了一年,刚好最近没有什么别的书,便花了几天给读完了。这本书由柏瑞尔·马卡姆编写,她向我们讲述了她童年到完成独自飞跃大西洋的经历,书中讲述了她在非洲度过的童年、参与狩猎、训练赛马、驾驶飞机等等各种经历。我们通过柏瑞尔的实际,了解非洲的自然风光和人文风情。同时,也能看到一位女飞行员对于飞行梦想和个人新生,勇敢面对人生的思考。

读库中看了 看懂《沙丘》这篇长文,之前只了解《沙丘》是一部有名的科幻小说,之前看过《沙丘》第一部的电影,第二部还没有看。看了这篇文章之后,才知道原来《沙丘》是非常出名的小说,而且有很多科幻小说中还有借鉴沙丘的部分。同时通过作者的解析又了解到《沙丘》其实不仅仅是科幻,而且继续还有中世纪战争的影子,以及解读出了地球其实也是沙丘。通过这个解析,之前看电影时候的很多迷惑感觉霍然开朗。

关于影视,最近看了迷雾剧场的《错对》,一部悬疑,案件侦探的剧,由一个案件,从而引出另一个案件,构思还不错。但是剧中女警察最后离婚还是看不懂。

另外就是这几天开始看Android系统的源码,刚刚差不多把Activity启动看完了。虽然系统的代码越来越多,但是整体上核心的流程变化倒是不大,抓住主线,关注核心代码,看起来还不算太累。后面继续加油,争取这次能把核心模块的流程都看一遍。

七月就这样过去了,虽然出去玩很开心,但是修车花钱很不开心。后面还是要小心,不能让车子遭罪了。也希望八月份能有好运势,事事如意吧。

最后以《夜航西飞》最后的一句话结尾吧: 我要是能从”银欧“走出来就好了,直到这愿望失却了意义,而时间继续前行,战胜一路上与之相逢的许多事情。

看完评论一下吧

记录再次编译Android系统的坑

2024-07-29 11:05:34

之前已经多次编译过Android系统的代码,但是一直没有静下来去阅读Android源代码。最近不太忙,决定开始好好读读系统源码。这篇文章作为开篇,先记录把Android系统编译出来,并且把源码阅读的环境准备好。

首先介绍一下,这次使用的是Ubuntu22.0.4 LTS版本,起初准备使用虚拟机安装,但是映射虚拟硬盘老是出问题,还是直接搞上了Windows & Ubuntu双系统,Ubuntu安装到移动硬盘里面,插上移动硬盘就能用Ubuntu,拔掉还能使用Windows系统。为什么要用移动硬盘,因为官方说了至少要留400G空间给源码和编译所需,不过我最后测下来300G也是够的。

为什么选择Ubuntu 22.0.4而不是最新的Ubuntu 24.0.4,是因为22.0.4后面的版本移除了libncurses.so.5,在22.0.4版本的时候我们还可以通过以下的命令安装,而后面的版本我们可能就只能使用android源码提供的相关库,并且自己去做文件的映射处理,反正我试过之后发现还是有问题,就重新安装了Ubuntu 22.0.4。

1
sudo apt install libncurses5

除此之外我们按照官方教程来做就行了。

首先是安装必备的软件工具包:

1
sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev libc6-dev-i386 x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig

安装repo,可以自己下载最新版本,也只直接使用ubuntu的软件包安装:

1
sudo apt-get install repo

设置git的用户信息:

1
2
git config --global user.name "user name"
git config --global user.email "[email protected]"

创建一个目录,然后在这个目录下面初始化Android系统 Repo

1
repo init -u https://android.googlesource.com/platform/manifest -b master

-u后面是Android仓库的清单文件地址, -b是我们要拉出来的代码分支,可以是分支名称,也可以是tag名称,我选择的是Android 13的源码,用了这个tag: android-13.0.0_r83

然后就可以调用repo sync来下载代码了,这个过程可能需要等待几个小时,看你的网速,可以在后面加上 -c -j线程数来加快速度,-c表示拉当前分支,-j开启多个线程下载,如:

1
repo sync -c -j16

下载过程中如果中断了,重新执行这个命令可以继续下载,如果有遇到错误说有.lock文件,去子文件夹的.git文件夹下面找到相关的lock文件删除再重试就行了。

下载完我们在工作目录,首先执行以下代码,初始化工作环境和一些命令:

1
source ./build/envsetup.sh

执行以下命令,初始化我们要构建的目标:

1
lunch sdk_phone_x86_64-eng

以上这两句,我们需要每次启动一个终端或者重启电脑后都需要运行,不然m和emulator等命令都用不了。

然后后面的目标也可以不写,这样会进入一个选择列表让你选要构建的目标。 之后就可以输入m来构建系统了。

构建完系统在命令行执行emulator理论上就可以在模拟器中运行我们的系统了。但是我这里模拟器确黑屏了,只有一个小窗口,运行失败,命令行日志只看到libvulkan.so移动失败,但是看了以下模拟器的目录下面,是有这个文件的,然后在这台电脑上安装了android sdk,在其中创建了AVD,并启动它,发现是可以的。 这个时候想到一个妙招,就是把sdk的模拟器拷贝到我们源码的模拟器目录,把这边的模拟器给替换掉。然后神奇的事情发生了,我们编译出来的系统运行成功了。如果你遇到类似的情况也可以这样试试,把android sdk目录中的模拟器复制到./prebuilts/android-emulator/linux-x86_64目录下面。

关于阅读源码,之前大家都是使用idegen来生成适用于Android Studio的工程文件,但是默认会把所有文件都导入,打开速度极慢,我想这可能也是我之前无法把代码阅读下去的一个理由。在去年,android官方推出了Android Studio For Platform,可以这里下载: https://developer.android.com/studio/platform ,UI跟Android Stuido一样,不过它可以帮我们自动执行source 和launch命令,以及对于platform的module的设置,导入的时候我们选择自己要构建的target就行了,使用几天下来是很好用的。

除了使用Android Studio platform 在Android 10之后google还为我们提供了AIDEGen这个工具,我就没花时间去用了,感兴趣的可以看看这个博主的文章:https://juejin.cn/post/7276812358663733263

以上就是我这次的编译过程,期待对大家有用。在此立个Flag,后续把阅读Android源码的内容也写出来。

一些参考的资料:

  1. 下载 Android 源代码-官方资料
  2. 针对 AOSP 开发进行设置(9.0 或更高版本)-官方资料
  3. 构建Android-官方资料
  4. AndroidStudio导入Android系统源码

看完评论一下吧

记第一次事故车险出险

2024-07-21 17:52:33

提车接近一年,月初刚刚带着家人一起去山东自驾,路上自己倒车撞到渔船螺旋桨把后挡风玻璃撞碎了,才有了这次出险记录,也是人生第一次车险出险,有损失,也长记性。

平时小擦小碰都当没看到,之前一次自己把底漆都刮掉了,就自己用补漆笔补一补完事。这样做也都是担心第二年保险涨价,车友群里保险涨价的不在少数,新能源车保险价格又比较贵,而我上个月续保也是因为有违章保险未降价。因此撞到后第一时间没有想着去报案,而是联系了当地的维修店。到店了解到需要订货便联系了上海的小鹏服务中心,问了报价之后觉得还是要走保险,但是啥也没做。

在第二天回上海途中,小鹏之前联系的售后告知需要先到保险公司报案,然后就照做了,但是分配的查勘员竟然是途经地的查勘员。对方以没办法现场查勘,没有事发后立即报案,没有事故认定为由,需要免赔30%,也就是说修车费用我需要自掏腰包30%。打电话给上海的太平洋被推卸责任,小鹏的人听闻我不在上海后说上海是可以通过行车记录仪录像辅助佐证的,然而查探员仍然不认同,告知我需要回到事发地找交警开事故证明方可。对于已经跑了几百公里路的我来说这是不太可能的,只好妥协接受免赔30%。

回到上海到小鹏店里定损,负责定损的员工告知我可通过行车记录仪佐证,我心里暗喜难道有转机。然而没想到过了两个小时就接到太平洋的人的电话,说我是否之前同意了异地勘察的免赔30%,他说已有这个意见无法修改。 后又接到小鹏工作人员电话说定损会定挡风玻璃加上后面的漆面,但是漆面面积很小就不修了。想想这样也可以,就接受了。 最后理赔下来,定损2650元,实际赔付1855元。而后挡之前告知的价格是2300,就很纳闷了,不知道是小鹏给我打折了还是怎么地,就没问了。 过了几天,挡风玻璃换好了,之前门把手收不回去的问题也修好了,小鹏还给车子把电充满,把车子送回来了。回来后,去车衣店,把掉漆的这块地方补一下车衣,漆面就由它吧。此事算是完结。 回头来看,处理这是事故过程中犯了不少错误。最先是倒车的时候不能只看倒车影像,这次就是因为螺旋桨在摄像头和雷达盲区之外。其次是撞到之后没有报案,也没有现场拍照,为后续理赔留下了大麻烦。然后是,在跟上海的售后沟通过程中起初没告知对方我当前在外地,引起异地报案存在免赔的问题。最后是,在勘察员的威吓下低头同意了免赔,否则等到回上海处理可能又是另一番结果。

吃一堑长一智,下一次即使是单车事故也要及时拍照,并且视情况报警或者打电话到保险公司报案。开车的时候也要更小心了,特别是倒车时候倒车影像,内后视镜,后视镜等等都要仔细观察。同时遇到自己不清楚的要多多问,在案发地的修理厂,如果当时问清楚了,当场就报案,即使是要交警开事故证明,也会更容易一点。还有就是找别人办事时候要讲清楚,比如最初跟我对接的小鹏上海的人,他就以为我在在上海,事故也在上海,可能告知他是在外地发生的事故,说不定就能更早得到更多建议。 总之呢,希望下次不要再出事故,也希望我的事故能够给其他没出过险的车友一个提醒,避免下次万一出险了,别像我这样这么麻烦。

看完评论一下吧

想去海边-山东半岛自驾游

2024-07-12 20:34:31

小朋友放暑假了,工作也不忙,而梅雨季的上海天天都在下雨,就想要来一次说走就走的旅行,奈何一家三口都病了,直到小朋友挂完三天点滴差不多好了,终于启程了。这一次简单的构思了一下目标就是一路往北跑,沿着海岸线玩水。

Day 1 日照万平口海水浴场

提前一天已经把车子送到了服务中心,把座椅通风异响和空调风道弄了一下。早上从上海的家里启程,在服务区吃个午饭充个电,下午到达日照。往海水浴场去的路上吃了此行的第一顿海鲜大餐,团购的2-3人餐,只要150,应该算是这一程最便宜的一顿了,三个人把一锅的蒸汽海鲜加上鲈鱼吃完,鲅鱼水饺和疙瘩汤就只能尝一尝味道了。随后便驾车前往海水浴场,玩到天黑透,小朋友仍然不愿意回去。

Day 2 青岛八大关

由于前一天车子的门把手收不回去了,预约了今天去车子的服务中心检查,一早便出发了,然而由于配件订货需要时间,暂时就不修了。路边找地方吃了个包子,就驾车前往青岛。

在八大关附近停了车,附近吃了个饭,喝了个崂山可乐,便溜达到了附近的海滩。此时正值一天中最晒的时刻,劝阻小朋友许久才终于离开海滩。在八大关景区转了了,公主楼和蝴蝶楼人也挺多,就没买票进去。天气看起来开始下雨,就开车前往预定的住宿处,安顿下来。

晚上吃完饭,开车到达石老人海水浴场,天仍在下着小雨,天也已经黑了,海边已没有几个人了,只有一些在用金属探测器检测海滩的工作人员还几个游客,我们转了一会也就回去休息了。

Day 3 青岛石老人海水浴场,威海荣成那香海

早起吃完饭,本打算前往崂山景区。驾车过程中又经过了石老人浴场,便进入游玩。给小朋友换上泳衣,带上泳圈,带他在海浪中玩耍,玩了许久仍不愿离开,一直玩到接近中午,看来崂山也只能留到下次再来了。要到了中午吃饭的时间,终于把小朋友劝中了,给她冲澡才发现身上已经晒黑了许多,穿衣处和裸露处色差很大。

下午到达那香海,这一片连着好几公里的海边都是沙滩和海水浴场,相当热闹,也有许多游客。然而一路都不需要车辆掉头,开了好久才成功掉头,在海边的停车场停下。当即决定,把车里的天幕搭起来遮阳。小朋友在海边玩水,捡石头,累了到天幕下休息一下。就这样一直玩耍到太阳沉下去,在海边吃完一顿烤串。

可能这是这一趟最开心的一天了。

Day 4 威海鸡鸣岛,威海公园

早上赶往鸡鸣岛码头乘船,买了海鸥爱吃的淀粉肠(不是薯条🤣),乘船前往鸡鸣岛,往来鸡鸣岛的船很多,几乎不用等待就开船了,很多海鸥追逐着我们的船,很多人在甲板喂海鸥。

没多久就到了鸡鸣岛,这个岛不大,花了两个小时就沿着岛转了一圈,据说是因为拍摄《爸爸去哪儿》这个岛才火起来的,岛边很多海鸥等待游客喂养,岛边有一些礁石比较好看,除了灯塔,龙王庙之外,岛上还有几个有特色的咖啡店,岛民经营的民宿餐厅等。岛不大,商业气息也不太严重,还不错。

离开鸡鸣岛,就驾车到威海找了个商场吃饭。吃完饭前往威海公园,沿着海边走走停停,看别人抓鱼,看游人骑行,本想去看那个大相框,走了许久才发现走错方向,赶紧调转方向。终于走到大相框,发现人太多了,根本没法拍照。就又返回车上,搜索了当地的点评必吃榜,去葡萄滩附近吃海鲜了。

点了很多的海鲜烧烤,有一些也是之前从来没吃过的,酒足饭饱之后又走到了葡萄滩海水浴场玩耍一会,结束了一天的行程。

Day 5 威海火炬八街,烟台养马岛

早上吃了当地的特色海菜包子,但是感觉到没啥特色,还是肉包子更好吃点。吃完就赶往火炬八街,但是还没走到,小朋友就被沿路的一个体感农场黏住了不愿意走。等她玩够了才继续往火炬八街走,途经的路还正在修,完全不像景区的样子,到了火炬八戒,又是人山人海,各种拍照的。在火炬八街的海滩玩了一会,就决定找个地方去吃饭了。

找了一个当地的老店,吃了个网红海鲜大咖,店里客人挺多,服务一般,不过价格也还算实惠,人走的差不多了,老板过来解释客人多服务下降望原谅。

下午赶往养马岛,本来计划走环岛路,然而前往獐岛的路在修,只好从岛内绕行。獐岛区域的海,虽然说没有达到果冻海的程度,但是也很漂亮了,应该是这几天来最好看的海面了。

随后沿岛开了半圈,景色大同小异。便决定去吃个海鲜面再看日落,然而绕路加上导航错误,吃完海鲜面已经看不到日落,岛上找了家民宿就住下了。

Day6 车祸现场,烟台蓬莱阁

早上本打算到海边再看看养马岛的海,然后看完海,倒车的时候,撞到了渔船的螺旋桨,把后挡风玻璃干碎了。打了4s电话就赶过去看了,然而玻璃也要几天时间订货修不了,就走了(也没报案,为后续修车问题留下了伏笔,等修完车再写吧)。

4s店告知修不了之后,便找商店买了透明胶带把后挡风粘上,防止后续行驶过程中,玻璃渣子掉下来。此时我们已经在烟台市区和蓬莱两地中间的一个区域了,便决定前往蓬莱去看看蓬莱阁。

蓬莱阁景区很大,附近还有很漂亮的欧式城堡和八仙过海景区,但是我们只钟情于蓬莱阁。蓬莱阁和滕王阁、黄鹤楼、岳阳楼并称中国四大名楼,自此除了岳阳楼,其他几个都打卡了。蓬莱阁建于海边,阁边观海很凉爽,景区内还有炮台,苏公祠,天后宫等建筑,也都走马观花看了看。

其中印象最深刻的是戚继光纪念馆,居然有接近一办的内容和主席有关。

本欲前往田横山看看黄渤海分界线,然后索道维修,不想爬山,便放弃了。打算回烟台市区看看烟台山景区,到达的时候,那边已经关门也只好在海边看看。顺便在虹桥1920转了转,那边的店铺都看起来比较高大上,也就未做停留。

烟台老城区,路上相当堵,驾车前往烟大小吃街附近,花了好久没找到小吃街,最后在路边吃了个烧烤,吃完烧烤终于找到了小吃街,原来也都是烧烤店铺。

Day7 日行千公里,冒雨回程

今日准备返程了,行到青岛,小鹏服务中心的人让我先报案,结果一顿折腾搞得心情不好,再加上玩了几天也有点累了,就决定直接开回家了。

过了连云港之后,就开始下雨,到南通之后,雨更加的大了,但是最终还是在晚上10点多到家了,此行也算是结束了。

总结

这是可以说是说走就走,也没列什么计划,酒店住宿也都是当天才定。 住宿的价格方面,威海和日照还算比较便宜的,青岛和烟台的价格感觉就要贵不少。同时吃饭反面也是差不多,日照最便宜,威海其后。评价海边的风景,养马岛的海水最好看,青岛和那香海的海滩最干净,烟台市区、蓬莱、威海市区的海要差一点,有一些漂浮物和海草,青岛的沙滩最干净,沙子更细腻一点。总体来说山东的沿海城市都不错,不太热,海鲜价格还实惠,是亲子出行的好去处。

一路高速上开了小鹏的NGP减轻了很多的驾驶疲劳,让我最后一天即使开了1000公里也感觉不是很累。但是最后一天ngp的时候方向盘乱晃,手打方向盘硬的问题,还是需要去店里看看。

开车去旅行,时间的安排更自由,一个地方不喜欢可以说走就走,当然出事故也也比较烦😭,总体还是方便更多。

此行花费的大头还是吃,其次才是住宿和交通费用,下次倒是可以买点海鲜自己做就会省下很多了。

虽然带了相机,手机,运动相机,结果整理照片的时候还是发现很多景点没有拍照,一是人多不想拍,二是觉得这个地方自己拍不好,归根结底可能还是技术菜吧。

看完评论一下吧

六月小记

2024-06-29 15:52:33

不知不觉,六月已到月底,2024年已经过半。工作上无所事事,又是摸鱼的一个月,博客也没有写一篇,想着还是要记一点东西,不然就这样又苟过一个月,又没有留下任何的记忆。

手术

手术实际上是五月底所做,做的是卵圆孔闭合手术。发现卵圆孔未闭合的原因是一直有头痛的症状,发现了上海六院有一个头痛整合门诊,结果查到了别的都没有问题,唯独卵圆孔未闭合,很有可能就是因此引起的头痛,遂进行了该手术。因为是微创手术,回家一个多星期伤口差不多就回复了,正常生活没啥影响。但是现在是夏天,术后严禁烟酒,不能喝啤酒就太难受了,只好各种法子的气泡水加冰加饮料折腾喝。另外就是不能剧烈运动,这段时间一直在家窝着,也不太敢开车出门。

手术的费用方面,医院的费用加上院外买的药总共5万元,医保覆盖了大部分,公司的商业保险覆盖了其他的部分,甚至医保个人支付的部分也给报掉了,变相的把个人医保余额给取现,因此来说保险还是起了很大的作用,还是很有必要的。

出行

因为需要静养的原因,再加上下半个月上海进入梅雨季节天天下雨,这个月机会没有出门。趁着周末在上海转了两次。一次是跟着朋友一起驾车到奉贤看海,走一走上海最美公路塘下公路。碧海金沙没有去,其他地方,也只能在堤岸上看看,海水不好看,并且也不能翻越堤坝过去。往塘下公路去的路两边水杉树好看,但是因为小朋友要睡着了,开了一会就返程了。

另一次今天,趁着今天没雨开到了青浦,逛了一下练塘古镇和金泽古镇。两个古镇都是商业化比较少的,练塘古镇就一条河和几座桥,外加陈云纪念馆,金泽古镇要更大一点。古镇皆为一条主河,河两边为民宅和店铺,河中偶尔还有一些小船,金泽是有游船的,因为饭点到达便未乘船游玩。除此之外金泽古镇还有两座小庙,为小镇更添几分热闹。但是总体而言两个地方人流都不太多。周末过来转转还不错。看完古镇又前往淀山湖吹吹风,之后惬意的回家了。

养鱼

之前给小朋友养了几只小螃蟹因为死了一只其他的怕是养不活,便全部放生了。看着网上别人养鱼手痒痒,就想着带小朋友一起养养鱼。说干就干,趁着6.18把鱼缸,水草泥,鱼缸灯等一堆东西火速买齐,收到快递后第一时间把缸给开了,当天又带着小朋友去花鸟市场买了几只斑马鱼放进去。随后几天又放了几条孔雀鱼,然而放完孔雀鱼当夜,就有两条孔雀鱼跳缸而亡。如今已过一周余,缸内已经开始长起了藻,缸内不只哪条鱼生了两只小鱼。每条小朋友来喂鱼,看鱼,给大人小孩都是找来了很多的事情干。

折腾

家里目前是用一个树莓派插了个硬盘盒,树莓派刷的openwrt,这样实现了家庭的全局科学上网,以及简单的NAS功能。但是之前自己编译的openwrt因为选择的kernel的版本的原因,许多的软件装不了,想要装docker扩展更多的功能也不行。

突然又想再折腾一下,遂重新拉了一下 ImmortalWrt 23.05.2 r27625-416c8c5c91 的openwrt,顺便把rom size改大到SD卡的剩余可用大小,这样之后根目录下面就有更大的空间方便安装更多的软件或者docker容器。

之后又分别安装了Jellyfin, Navidrome,FreshRss实现了视频,电影,RSS订阅的服务。如此下来家庭环境下就有了更好的影音体验。唯独不足的是硬盘只有2TB,拷了一些电影之后就快满了,而把另一块放到一起弄Raid0或者JBOB, Jellyfin的识别有问题,又或者拷贝大文件磁盘就Down了。想一想再要折腾就要买成品NAS或者自己买硬件组装,反正是不能用USB硬盘盒了。

读了两本书,一本马伯庸的《太白金星有点烦》,以太白金星的视角来写西游记,写出了官场职场的人情世故,相当精彩。另一本是刘易斯的《大空头》,详细记叙了在08年次贷危机前,巴黎,艾斯曼等人做空次级贷款的故事,书中有次贷是什么的记叙,有他们如何发现次级贷款存在问题的记叙,还有贪婪和傲慢的华尔街投行们。这两本书都很值得 一看。

陪着老婆看完了两部爽剧,一个是美剧《布里奇顿家族》,一个是中国的古装爽剧《墨雨云间》,两种不同的爽剧,皆有主人公甜甜的爱情,都是下饭的好东西。另外还开始看《人生复本》, Apple TV出品,内容是关于多重宇宙的,刚看了两集也不错,另外这个是根据同名的书改编的,准备先去看看书。

总体来说,这是摸鱼的一个月,工作上没做啥事情,上班时间把南大的操作系统课给刷完了,JYY讲的很精彩,关于系统调用,内核态等等各种东西,让我在看java虚拟机和协程的时候有很多地方感觉更通顺。这种讲的比较好的基础类课程还是值得我们去刷一刷的。

好的,这个月就这样结束喽。

看完评论一下吧

向伟人学习-读《富兰克林自传》

2024-05-25 20:05:21

最近Apple Tv上映了《富兰克林》,想到了《富兰克林自传》这本经典书还没看过,遂找到来读。书的内容大体上记述了他少年,青年,中年,以及到50多岁的事情。书的内容很好,很有启发,也有很多他的个人品质值得我们学习,看了一遍恐难以很好的对他有所了解,第二遍读完方才动笔开始写读书笔记。

生平简介

恐怕有人对富兰克林不了解,我这里简单介绍一下。他出生在一个普通人家里,父亲移民到美洲后生下他,他年少就作为一个印刷工,后来自己开办印刷所,同时发行报纸等,在殖民地他积极参与议会并担任职位,后期参与推动了美国独立,他积极投身于公共事业,提议创建了消防队,巡查队,参与创建图书馆,建立宾夕法尼亚大学等等。空闲时间还进行创新发明,进行科学实验。关于他的更多详细介绍,感兴趣的可以去看看维基百科

读书与学习

富兰克林仅仅在学校里面读了两年的书,但是他重新就酷爱读书,自己的零花钱都会拿去买书看,十二岁进入哥哥的印刷所之后有了更多的机会接触到更多不同的书,因此也有机会阅读更多的书。而之后,不管是去到费城的印刷所工作,还是在英国的印刷所过做,以及后面开了自己的印刷所,他都一直保持读书的习惯。我们可以看到在当时获得图书很艰难的条件下他仍然孜孜不倦的通过各种方法获得图书去阅读,这启发当下的我们更应该保持终身阅读的习惯。

而他看书还不是看一下就完了,书中他分享了年少时候的他发现自己的无法对于词语运用自如,便尝试把故事使用诗歌的形式来转述,即锻炼了自己运用词汇的能力,又很好的去用自己的语言去理解原文的内容。不仅如此,他会在过一段时间自己把诗歌还原成散文,把自己的与原文对比,改正自己的错误或者发现原文的错误。类似这样的用自己的语言来复述原文,并进行订正的学习方法,在当下看来仍然是很有效的方法,依然值得我们来学习。

不仅仅如此,他还擅长通过观察来学习。在他十二三岁的时候他通过观察学习工匠技术,作为一个印刷工做的比大多数的工人要好。在家的时候他也通过观察发现自己父亲的特质,已经听哥哥的朋友们谈论学习。

在十几岁的时候,他就结交喜欢读书的人,并且经常一起互相交流读书感悟,并就相关内容进行辩论。他把他的朋友们组织成一个俱乐部,互相促进,共同提高。向别人学习,从交流中学习,这也是一种很好的学习方法。

品格修养

他为自己提出了十三条的美德规诫,并且花了很长的时间来进行来培养这些习惯,书中也讲述了如何培养这些习惯的方法。规诫内容如下:

一,节制。饭不可吃胀。酒不可喝高。
二,缄默。于人于己不利的话不谈。避免碎语闲言。
三,秩序。放东西各归其位,办事情各按其时。
四,决心。决心去做该做的事情,做就做到心想事成。
五,节俭。不花于己于人没有好处的闲钱,杜绝浪费。
六,勤奋。珍惜时光。手里总忙有益之事。剪除一切无谓之举。
七,诚信。不害人,不欺诈。思想坦荡,公正;说话实事求是。
八,正义。不损人利己,伤天害理的行为永不沾边,利公利民的应尽义务切勿放手。
九,中庸。避免走极端。忍让化冤仇。
十,清洁。身体、衣着、居所,不许不干不净。
十一,平静。不可为小事、常事或难免之事搅乱了方寸。
十二,贞洁。少行房事,除非为了身体健康或传宗接代;千万不可搞得头脑昏沉,身体虚弱,或者伤害自己或他人的平静或声誉。
十三,谦卑。效法耶稣和苏格拉底。

这些品格对于我们现代人来说仍然很有用,特别是当今网络发达,社会上各种光怪陆离。他提到的一个一个的习惯养成,循序渐进,同样适用与我们养成其他的习惯。

从他做的很好事情中,可以看到很多他的好品格。经常他提出的很好的建议,他常常会说自己不是首创人或者只是合伙人之一,这无疑体现了他的谦卑。同时他的这样做法会让人感到他不居功自傲,更容易把提出的提案推行成功。

事业与财务

他从一个印刷工学徒发展到印刷商,从两手空空到通过印刷所的收入可以养活自己一家,而自己可以全身心的投入到政治,公共事务和科学研究中去。他靠着自己过硬的技术和良好的人格,获得了朋友和工友父亲的投资,使得他开始自己的印刷所,以及后面他通过与他的工人合伙创建更多的印刷所,实现了钱生钱,利滚利。

这本书上我们可以看到一个爱读书的年轻的奋斗史,他有很多的地方值得我们学习。当然,他写书本身也是想通过自己的经历来劝诫年轻人,可能他的一些美德有被强调,这倒是方便我们来关注到。 到这个年纪才读了这本书也是我的一大遗憾,这本书已经是中小学必读书单之一了,我也是强烈建议中学生都能读一读。学习富兰克林的美好品德,学习他爱读书的习惯,学习他的学习方法。

最后推荐大家亲自阅读。

看完评论一下吧

一个Android开发者的Google IO 2024信息汇总

2024-05-21 21:53:44

AI和大模型很火,今年的google io上面感觉各个方向都和AI有关,Android平台相关的东西倒是感觉不太多了。我这里整理一下Android相关的信息。Android主要就是Android 15的发布,以及jetpack compose的更新和google play的一些更新。

AI与Android

Android 14开始手机会内置Gemini Nano,开发者可以通过AI Core来调用AI的能力,目前是google pixel 8 pro和三星的s24已经内置了。除了端上,开发这还可以使用Firebase新提供的一些能力使用AI模型。

对于开发者来说,除了开发应用提供AI能力给用户,开发过程中也能体验到google提供的AI能力,下载Android Studio Koala即可使用,提供了类似github copilot的功能,有代码补全,自动代码生成,对话生成代码,UI设计图生成代码,代码重构等功能。

Android 15

每年一个大版本现在是Android系统的惯例了。google io开始的这天发布了Android 15 Beta2。

摄像头方面引入了低光增强功能,这个对于国产手机拍照功能基本都是已有功能,不过对于开发者可以通过Camera2的接口来使用相机提供的这个功能。同时还提供了新的闪光等强度控制API,在支持的设备上更好的控制相机硬件和算法。

文字方面,中日韩语的字体,android上的是NotoSansCJK,新的版本将是可变的了,这代表可以中文可以有更多的样式和变化了。Android 15开始,可使用 JUSTIFICATION_MODE_INTER_CHARACTER 利用字母间距将文本两端对齐,Android 8.0引入了字词间对其,这对于中文这种单个字符的语言是不友好的,新的字符间对其可以大大改善中文排版的美观度。Android 15中还提供了标记来控制换行,即 <nobreak>来避免换行,<nohyphen>避免断字。

1
2
3
<resources>
    <string name="pixel8pro">The power and brains behind <nobreak>Pixel 8 Pro.</nobreak></string>
</resources>

未使用nobreak 使用nobreak

对于GPU的使用,Android在7.0的时候引入了Vulkan,在今年他们有引入了ANGLE,这个对于普通开发者不需要关注,对于游戏开发者或者需要使用Open GL进行图形处理的需要注意,毕竟Google计划未来只支持通过ANGLE使用Open GL。

预测性返回动画之前在android14中就有了,但是需要在开发者模式中手动打开,普通用户还用不了,现在默认打开了。

最小target sdk version提高到24,低于此版本的将无法在Android15的手机上安装。

另外关于前台服务,隐私空间,系统UI等方面也有一些提升和新功能。更多内容可以看Android 15的文档: https://developer.android.com/about/versions/15/summary

Kotlin和Jetpack compose

Kotlin 主要介绍了jetpack的更多库适配了kmp,让开发者可以在更多平台共用代码。具体可查看文档:https://android-developers.googleblog.com/2024/05/android-support-for-kotlin-multiplatform-to-share-business-logic-across-mobile-web-server-desktop.html

Jetpack compose支持了share elements 动画,列表动画,Text支持链接和html富文本而不必使用ClickableText,以及一些性能提升。当然,最大的变化应该属于在kotlin 2.0的时候,compose的编译器将从谷歌的代码库移到kotlin的代码库,对于我们来说这意味着compose的编译器插件版本将和kotlin一样,减少我们升级版本的很多烦恼。更多详见:https://android-developers.googleblog.com/2024/05/whats-new-in-jetpack-compose-at-io-24.html

同时Compse对于更多尺寸的屏幕,以及手表,电视等等有了更好的支持。compose adaptive 库提供了一些api来让我们更多的适配各种屏幕尺寸,主要的是NavigationSuiteScaffold, ListDetailPaneScaffold, SupportingPaneScaffold。这次大会更新了Compose for wearos 的库,让更多功能稳定下来。正式发布了Compose for TV的1.0。这样下来所有的Android 平台都可以使用Compose进行开发了。

我们的桌面小组件,也发布了Jetpack Glance 1.1,来让我们支持使用Compose来编写桌面小组件。当然其中的一些widget和普通compose拥有一样的名称,但是却来自不同的package,因为最后还是会编译成remoteview,因此不能混用。

由此可见Android的原生开发以后将是Compose的天下,加油学吧。

Flutter

Dart支持了宏,从而对于Json的序列化和反序列化会更加简单。具体的使用方法: https://dart.dev/go/macros

Flutter 通过WebAssembly在浏览器上面运行性能更好,因此官方后面在Web的方向应该是WebAssembly了, 对于Flutter转js这个方案应该会被放弃。

Google Play

谷歌正式放出了隐私合规检查工具 Checks,可以帮助我们检查app的隐私合规的问题,检查app收集的用户数据,使用的权限,sdk等等,在当前各个国家对于隐私政策越来越严的当下,这个东西挺不错的。访问官网了解:https://checks.google.com/

谷歌在2021年发布了Google Play SDK Console给一些大的SDK开发者,现在这个Consle开放给了所有SDK开发者,借助这个平台SDK开发者可以在sdk有漏洞或者安全问题时,让使用到sdk的用户在谷歌得到通知来升级sdk。同时还可以查看sdk的用户数据,以及让应用开发者共享sdk的崩溃和卡顿日志给sdk的开发者,从而更快的解决问题。

谷歌还发布了Engage SDK, 帮助开发者在google play 内展示app的内容,吸引用户使用我们的应用,但是这个SDK需要开发者在媒体体验计划中或者应用用至少100K的DAU才能申请。当然除了这个sdk,新的google play还支持我们使用搜索关键定制商店详情页,这样可以对不同的来源做定制,提高用户下载app的转化率。应用详情页之前只能展示针对当前设备的屏幕截图和介绍,新版本将支持展示所有支持的设备的信息。

Play Integrity API也有更新,新增加了应用访问风险检查,服务端接口也可以使用play protect的验证。

新版本的Google play后台中还支持对于Deeplink 的管理,通过Google play甚至可以变更deeplink, 而不用更新app。

更多的内容还是请参考官方内容:

Android系统的更新:https://developer.android.com/about/versions/15/summary

Android Studio 的更新: https://android-developers.googleblog.com/2024/05/google-io-2024-whats-new-in-android-development-tools.html

Google Play的更新: https://android-developers.googleblog.com/2024/05/io-24-whats-new-in-google-play.html

Compose的更新: https://android-developers.googleblog.com/2024/05/whats-new-in-jetpack-compose-at-io-24.html

Flutter 更新: https://docs.flutter.dev/release/whats-new

看完评论一下吧

看皖南山水-皖南川藏线及查济桃花潭自驾游

2024-05-08 20:42:32

皖南常去,但是很多景点一直没去过,特别是皖南川藏线和查济古镇一直心心念念了好久了,这次趁着五一自驾转了一圈。因为遇到下雨,此次行程耗时三天,很多地方匆匆而过,因为而且还没到玩水季节途中的很多漂流景点也未开放。一路山看山看水,看古镇,整体体验也还不错。 黄山太平湖

Day1

上海出发从宁国下高速,一路还比较通畅,两个多小时就到啦。给车子充满电,去网友推荐的餐厅吃个饭,就驶向皖南川藏线的东入口了。

第一站去了储家滩,路的一边是山一边是河,风景不错。第二站到达青龙湾观湖驿站,有一个看湖的观景台。山上的惠云禅寺是观青龙湖的好地方,但是考虑到绕一下可能看不到日落就没过去了。

落羽杉观景台的上山台阶

落羽杉

再往后走到了落羽杉观景点,有一个观景台需要从停车点后面爬上去,上去之后可以看到旁边的水杉林和河流,由于河水水位下降风景略有一点差。后面还有小岭头云烟山野不好停车就没下来看了。在去往幸福路观景台的路上看到一些猴子,不过不好停车也就直接走了。幸福路观景台和六道弯这一段的路就有很多的弯道了,真的是川藏线的体验了。

幸福路观景点

六道弯看夕阳

六道弯夕阳

六道弯咖啡店

到达六道弯正好太阳下到了山顶,夕阳倒是让我拍到了。时间不早就直接下山,后面的几道弯都没有停留了。下山后在苏红村找个民宿,炒两个菜,喝点酒,然后就帮着蛙声睡下了。

Day2

早上醒来,因为刚下完雨,外面的山顶浓雾笼罩,空间也很清新,打开窗户神清气爽。

水墨汀溪

水墨汀溪

吃完饭就启程前往漕溪花海,然而除了一些茶树啥也没看到。转而去了水墨汀溪,因为还没到时间,景区还不收费,车子可以直接进去。里面的景色还可以,溪流玩水也很不错。就是没来对时间,还没到漂流季节,内有皖南蜀道,小朋友直接被吓退也就没去了。准备去来就月亮湾家下起了大雨,路上买了点当地的茶叶带着。到达月亮湾雨还没停,也是没法玩了,就继续前行前往查济古镇了,路上找了一家土菜馆把午饭解决掉了。

查济古镇

到达查济古镇,雨也停了,景区门口的民宿老板招呼着,就开车进景区看了看民宿环境,把晚上的住宿解决了。听了老板的建议,随后就走到河边开始探索查济古镇。转了个大概又开始下雨便回去了。

Day3

早上吃完饭,准备在景区再转一圈,遇上了一大波过来春游的中学生,原本显得很冷清的景区热闹了许多。把财神敲我,红楼桥,二甲祠都又转了一下,买了点板栗饼就启程前往桃花潭了。

文昌阁

中华第一祠

南阳古镇

把车子停在啦桃花潭游客中心,随后便进去了,首先看到的桃树已经结了果,油菜也马上可以收割了。先去看了文昌阁,中华第一祠,南阳古镇,随后坐船到达对岸。坐船就能看到桃花潭了,桃花潭不是我们想象的一个湖,其实是青弋江中的一个点。湖对面是万村古街,河边的亭阁是个观潭的好地方,旁边还有汪伦墓。随后坐船回对岸,走到停车场,这时发现车子如果停在水东老街出口这里会更方便。

南阳古镇

踏歌楼

桃花潭

汪伦墓

本想继续在玩一天,老婆觉得有些累了,就决定开车回老家了。经过太平湖就找了方便停车的地方在这边转了转 湖水很绿,周围有山 风景很不错在湖边露营应该很惬意 看完湖就开车回家了。

其他

最后再说说电车充电的问题,车子满电400公里没啥问题,目前的单程 500 公里内的基本不成问题。上海到宁国不需要充电就到了,进皖南川藏线之前利用吃饭时间把电充满了。几个地方玩玩之后,在回老家的路上利用休息时间把电充满。总体来说,车支持快充,再加上利用服务区休息和吃饭的时间,基本上不浪费时间。

另外玩的过程还拍了一些视频,分别是: 皖南川藏线游玩视频:

查济古镇以及周边视频:

看完评论一下吧

记国产手机无法在Chrome使用Passkey问题解决

2024-04-23 20:30:12

众所周知,在国产Android系统上面,Google play service是被阉割掉的。部分厂商提供了打开google play service的选项,让我们可以登录google 账号,使用Google play store,以及部分的Google 云服务,但是Google password manager以及Credential Api确是没有的。在Android手机上面,Passkey是依赖于GMS和 Credential Manager API的,因此,国行手机上面也就没法使用passkeykey。不过使用Chrome浏览器的话,还是能够使用Passkey的,这是因为Chrome提供了相关的实现。然而,前几日Chrome升级后,我的Passkey突然就不能使用了。

首先尝试了重新卸载重装Chrome,手机的google账号管理里面测试passkey等等,结果还是不行。最后只得尝试在Google搜索,找了很多,发现了这样一个页面How do I use Passkeys in Google Chrome on Android?, 其中介绍的是如何在android手机上使用1password。其中关于chrome的flag部分引起来我的注意,因为Android 14后开始支持使用除了google password外其他的应用提供的passkey功能,所以我猜想可能是因为这个,google 最近改动了chrome关于passkey的逻辑,默认会使用手机系统的Credential Management Api而不是Chrome自己内置的Api。尝试了一下把这个Flag改为Disabled, 重启一下Chrome,Passkey又工作正常了。

操作方法为Chome地址栏中输入:

chrome://flags

然后搜索Passkey,出现这个条目之后,修改其设置。

另外还要说一下,虽然Chrome中的passkey使用问题解决了,但是因为手机内没有Credential Manager Api,应用还是没法使用passkey的。除此之外,通过手机扫码,让电脑使用passkey的时候,也会一直处在连接中,最后也会失败,目前这个也无解。因此,如果想要顺畅的使用Passkey只要两个解决办法,一个方式是换iPhone,另一个方法是买一个海外版的Android手机,比如Google 的Pixel,三星,或者尝试一下海外版本的ROM。

看完评论一下吧

我的个人密码存储与管理

2024-04-08 19:13:24

作为一个网民,使用每个服务都需要注册账号,而注册账号就需要设置用户名和密码。在早期,我会将所有的密码都设置成相同的,这样方便自己记忆,每次输入密码也都很方便。

很久之前的一个同事,他会将自己的所有密码都记在一个小本本上。彼时,一些使用iPhone的朋友已经开始使用1Password来存储自己的密码了。而我,作为一个坚定的Android用户,此时还没有使用过任何的密码管理软件的。

直到某一天,Chrome提醒我我的密码已经泄露不安全了。此时便开始研究适合我的密码管理软件。 最终选择了Keepass作为我的密码管理软件。

经过几年的使用,使用的软件终于稳定下来了,在此分享一下。

目前我需要查看软件的平台有三个Mac 电脑, Windows台式机,以及我的Android手机。 Android手机我使用的是:Keepass2Android ,windows和mac下使用的是 KeePassXC。密码库是一个kepass文件,可以理解为一个加密数据库,必须通过主密钥才能打开。客户端本身不提供密码的多平台同步功能,我本人使用了坚果云来存储kepass文件,Android手机上keepass2Android通过webdav访问和同步密码库, 电脑上使用坚果云的客户端来同步密码文件。

密码管理工具首先能满足的功能就是创建密码,三个平台的客户端都能自动的生成密码,并且允许配置密码的字符,长度,密码安全等级检查,软件也支持过滤弱密码。

一个做的比较差的密码管理工具,是需要用户在每次输入密码的时候都打开密码管理工具,来复制密码回去再进行粘贴的。这方面1Password做的是最好的,有很多的自动输入或者选择来提高易用性。kepass的客户端当然也是有的。

Android客户端首先是支持Android系统的自动填充服务的,Android 8.0以后的手机就支持,需要在系统设置中设置自动填充服务为Keepass2Android,同时密码保存的时候要保存当前这个应用的package name这样才能自动填充对应的密码。当然也可以先搜索到这个密码后,软件会保存package name,下一次就可以自动选中这一条密码了。对于不支持自动填充服务的手机,或者应用开发者关闭了自动填充,也可以输入的时候把输入法切换为keepass2Android(前提需要在系统的输入法中启用keepass2Android的输入法)。

对于电脑上面,可以通过浏览器插件来实现密码的自动填充,目前edge,firefox,chrome都有对应的插件,可以在上面的链接中找到。对于网页的自动填充,还需要在创建密码的时候,把网址填到密码信息中。

国内的大部分账号目前都是短信验证码登录,用不到密码。另外一些银行或者金融类的限制了自动填充,甚至自己写了个键盘,还是只能切到密码管理器去看密码再回来手动输入进来,短期内也不会有所改善。

总体来说,目前大部分的重要密码都保存到了keepass中,文件也是自己管理,比1password这种托管的更放心一点。目前海外已经开始使用passkey来替代密码,相信未来密码的使用会越来越少。

看完评论一下吧

一场美食美景之旅-记成都重庆游

2024-03-24 22:03:23

重庆和成都一直在旅游清单的列表中,月初朋友提议一起到成都走一走,想着三月份是成都重庆的旅游淡季,遂买了机票规划了这次的旅行。

四姑娘山双桥沟

行程计划

出发前先制定了此行的规划:

Day1 到达成都天府机场,换成地铁到达市区,住下,周边转转

Day2 去春熙路,ifs 熊猫拍照,人民公园,宽窄巷子,武侯祠,锦里,看川剧

Day3 租车去都江堰水利工程,卧龙中华大熊猫苑神树坪基地,四姑娘山镇

Day4 去四姑娘山双桥沟景区参观,驾车到成都东站还车,乘坐高铁前往重庆

Day5 山城步道->解放碑->十八梯->白象居->来福士->洪崖洞

Day6 鹅岭二厂->李子坝->长江索道->弹子街->重庆江北机场

订酒店的时候才发现原来成都在搞糖酒会,酒店价格都快翻倍了,于是设计了一个只在成都住两晚的行程。

Day1

到达酒店后,就看到附近很多的串串火锅之类的店,因为很累就在酒店旁边的肥肠鸡和串串店开吃了,配上啤酒相当爽。

酒足饭饱后,又跑到了春熙路和太古里看了看,熊猫雕塑打个卡。

Day2

早上起来在酒店旁边吃了千里香馄饨就前往人民公园去吃早茶了,发现都是游客😄。 人民公园离宽窄巷子不远,步行就过去了,成都各种熊猫饰品玩具是真的多,到处都是。 在宽窄巷子顺便看了个川剧变脸,虽然不咋样,但是也算体验了一下。

下午去武侯祠转了转,了解了一些三国的文化,想要更深入的了解还是请个导游更好点。这里的红墙道路很适合拍照,帮老婆孩子拍了几张,这里就不发了。武侯祠出了门就是锦里,这里和宽窄巷子看起来差不多,转了一会就走了。随后打车去了九眼桥,在附近吃了个老妈蹄花。待到夜幕降临,看看夜景,感受一下九眼桥酒吧街也不错(可惜带着小朋友没法去酒吧)。

Day3

因为行程比较赶,提前一天去取了车,早上早早的出发了。首先到达都江堰,这是雄伟的水利工程,当天还在进行大修,但是看起来比较枯燥,看完抓紧去下一个地点。

之后去了卧龙神树坪大熊猫基地,这里的游客比较少,大熊猫们都很在线,很活跃,小盆友们很喜欢。

随后驱车前往四姑娘山镇,路上随着海拔的提升感受到植被的减少,到达三千多米的海拔后居然下起了雪,为此相当失落想着在后面的猫鼻梁观景台应该是看不到雪山了。 随后下车方便,小跑了两步,明显感受到头晕头疼,遂放慢了脚步。之后穿过巴朗山隧道,发现隔了一座山居然是两种风景,一边是大雪纷飞,另一边却是晴空万里。到达猫鼻梁观景台,顺利看到雪山,由于时间关系未在这里等待日照金山,算是一点遗憾。

Day4

提前知晓了双桥沟景区是看不到四姑娘峰的,便选在长坪沟住,但是前天晚上下雪了,但是还是没有看到四姑娘峰,略有遗憾。 早上乘坐观光车,到达双桥沟的终点红杉林,一路上白雪皑皑,终点处还正下着雪。地上,树上都是雪,远处的山上也是雪,确实美丽。

从终点回来途中的景点,又有不一样的体验。

中间又几个措(湖),是拍照打卡的好地方。

观光车上山差不多就一个小时,还要回成都去重庆,时间还是比较赶的,再加上朋友高反有点严重,7个观光点就没有全去,之前别的游客推荐的一个很美的拍照点也没去,这个遗憾只好下次有机会再来补了。

之后便开车赶回成都前往重庆,一路开车还是比较累的,山路各种U形弯相当刺激,油车加速不给力,上个坡踩油门都没反应。但不管怎么说,晚上还是顺利到达了重庆。

Day5

睡了一觉起来后才感受到重庆的3D地形,明明是负一楼,电梯出来居然是外面的道路。明明爬了几层楼,出来居然是马路。住在解放碑不远处,出门就打卡解放碑。

中午吃火锅,朋友推荐的店在装修没吃到,便利店的阿姨推荐了旁边巷子里的火锅,真的好吃又便宜。

下午去山城巷,十八梯,感觉重庆哪有什么旅游淡季,不是节假日,还是工作日,每个地方都有很多人。

晚上去了洪崖洞,拍拍夜景,桥下的一片水洼映出的洪崖洞倒影非常出片。

Day 6

打卡李子坝地铁穿楼,依然很多人。

随后去了鹅岭二厂,很文青,比较适合拍照。中午吃了个江湖菜,是个川菜大集合,😄。 下午看了看朝天门和湖广会馆,就前往机场打道回府了。为什么没去长江索道呢,当地人建议不去,又要排队就放弃了。

总结

除了照片还做了个视频。

成都和重庆的美食确实多,晚上11,12点,八一美食街和其他的各种好吃街都还有很多人,虽然人多,但是下次还要来。 四姑娘山这次只留了一天时间,还没玩够,下次有机会再来体验一下长坪沟听说很刺激。 重庆人好热情,不认识的人会给你推荐好吃的店,出租车和网约车司机也不坑,给你有用的建议。去机场的司机还热情带看了重庆的盘龙立交。 古街古镇感觉都差不多,在重庆就没去了,也不推荐去,去重庆专注吃就行了。

看完评论一下吧

历史之城英雄之城-南昌游记

2024-02-23 18:44:29

春节的时间比较长,不出去玩一趟就感觉没有过完整似的。今年依然选择了江西,因为江西的城市离得近,开车就能到了。本打算去庐山,但是查了一堆攻略,临行前一晚又改到了南昌。南昌更远,不过也更加平价,老城区的建设比较慢,好吃的很多,总体还是很满意的。

第一天 - 滕王阁

驾车出发,下午2点多到达南昌,直奔滕王阁,因为堵车在外围转了一大圈,只好先定了酒店前往住下。

酒店出来,直奔蛤蟆街搞了点吃的。首先就就是南昌知名的水煮和油炸,水煮和麻辣烫感觉差别不大,油炸虽然和我们老家的油炸很像,但是配着他们的调料又感觉很不相同,这个调料真的很好吃。

吃饱之后,直奔滕王阁,然而来的太晚了,售票已经结束,在滕王阁公园和赣江边转了转。

随后,跟着人群来到了东门,这里很是热闹,有漂亮的灯笼,有很多摊贩。在这里购买了《寻梦滕王阁》实景表演来夜游滕王阁。

夜幕降临,滕王阁前面这条路上的灯亮了,此时可以看到很多诗句,譬如“落霞与孤鹜齐飞,秋水共长天一色”,相当有意境。

晚上观看实景表演,下着小雨,不过表演效果不错,通过表演看到了特技表演,也了解了许多之前不了解的关于滕王阁的历史和文化,也是值回票价。

随后夜登滕王阁,了解了更多关于滕王阁的历史,以及与他有关的人,以及江西的一些文人文化。登上高阁,也一赏赣江两岸的夜景。

第二天 - 寻觅英雄足迹

第二天一早,骑着共享电单车我们就直奔八一广场去了,想说去看一看八一纪念馆了解一下南昌起义的历史。然后后来发现需要预约,今天已经约不到了。只好在八一广场看了看相关的雕塑和介绍。

随后去到了隔壁的江西美术馆,美术馆的设计很红色,和八一广场很搭。展馆中的展览,有关于八一广场的,也有红色主题井冈颂,最令人印象深刻的是《虚拟与重塑》相当的现代和科幻。

午饭后去新四军旧部遗址看了看,了解一下新四军的发展历程和革命历史。

最后,我们到访了万寿宫历史文化街区,这里的街是很繁华,人是超级多。

第三天 - 看江博

第三天我们早早来到了江西博物馆,展馆中的特展御瓷归来相当惊艳,展品大多是从故宫博物院和景德镇御瓷博物馆借来的,布展精美拍照很好,我只顾着看了,就没怎么拍。

另外二楼的江西历史文化展,对于江西的历史有了更详细的了解。

从博物馆出来,去取车的路上又经过了建军雕塑广场,此处可以看到南昌老城区,又有一些建军相关的雕塑。

随后,吃完山寨老三样,即踏上归途。

南昌体验

南昌老城区的房子能够感受到历史感,红砖墙面,镂空花纹,以及各种不同样式的又年代感的建筑。而改造后万寿宫历史街区又与老房子形成鲜明的对比。

南昌的美食很多,虽然没有吃到正牌老三样,山寨店感觉也很不错,点了几个菜每个都好吃。另外,南昌的早餐有好吃的拌粉和瓦罐汤,而且很便宜,一份粉只要5块钱,只要上海四分之一的价格。除此之外,羊子巷,蛤蟆街,士大夫院街也有很多好吃的。但是如果不能吃辣,就很多东西吃不了了,南昌的辣感觉真的是湖南和四川没法比的。

总结

南昌的人真的多,本以为春节假期的尾巴过来回人少点,结果还是看人从众,看表演都还是加座区,吃饭排不上号。不过话又说回来,吃饭也没必要去网红店,其他店也很好吃的。

南昌有地铁公交,短途可以骑电单车,交警管的也管的很松,体验不错。反倒是驾车在老市区挺麻烦的。

另外开着电车过来,一路300多公里基本也不用充电,中间休息的时候补个电也节省时间,去看博物馆的时候,把电充完,回程也无忧。

此一行,对江西的历史文化了解更多,也吃到很多江西美食,很满足😋。

看完评论一下吧

2023年个人总结

2024-01-24 23:30:52

柿柿如意 图: 明明 又是一年的结束,又是一年的开始,看到很多人写了年度总结,才想到我的年度总结还没有写。终于马上要到农历新年了,匆匆动笔。

工作

对于身处的行业和公司来说是起伏的一年,行业经历了公司爆雷公司破产,我们公司也受到重挫。我们公司也裁员了三四波,到年底我们组的人数只有原先的一半。因为经历了几次需求做完最后又不上线了,很多人也从年初的干劲满满,到后期的无所事事,消极怠工。每次要裁员的消息传出之后,我也很是忐忑,一方面是现在的整体大环境不好,找工作难,另一方面是个人的落户等等方面会受到工作的影响。

给自己过去一年在工作能力和技术方面的评价,个人认为是成长很少的。今年所做的东西方面欠缺挑战性,个人在技术学习上所花的时间很少。

2023年是AI火热的一年,各种大模型得到应用,在我的工作中,Github Copilot极大的提高了编程的效率,一些模板性的代码,它能够自动补全,一些常见的写法它也能够自动生成。ChatGPT在很多场景下可以代替搜索引擎,给到的结果更加精准,减少自己搜索时候的筛选过滤时间,在学习新的编程语言和用法时也很好用。相信经过几年的发展之后,相关的技术会越来越好用。

个人成长

在个人成长方面有一些习惯在这一年还是做到了保持和延续。

首先是英语的学习,公司提供了italki英语课程,目前还在持续跟着两个老师在练习,虽然英语依旧蹩脚,但是学总比放弃要好很多。

今年依旧在坚持读书。订阅了《读库》,上面的一些内容写的很多不错,总体来说每两个月一期的MBook都读完了,内容总体来说是纪事纪实的,内容相对小众,收获了很多的新知识。一般都是在零散的时间来阅读这些东西,我认为花时间在这个上面比刷抖音短视频要好很多,因此今年依旧继续订阅。

另外今年的阅读种类更加丰富了,除了读库内容外,还有在微信阅读上看了大量内容,包括人物传记、小说,金融题材,心理学题材,个人成长等。人物传记读了毛泽东传、马斯克传,还读了李光耀观天下,这本不是传记但是可以感受到李对世界格局的观察。金融书籍看了聪明的投资者、查理芒格的穷查理宝典、金钱心理学等,增加了一些投资理财方面的知识,还从书中学到大师的智慧。心理学内容看了被讨厌的勇气,幸福的勇气等,计划阅读阿德勒作品,但是一直没看。今年读到的比较有意思的小说是马伯庸的《长安的荔枝》,记叙有趣,让人一次性读完,仿佛回到了当前,同时书中又记叙了官场的斗争,人与人之间的相处等等。个人成长方面,看了《纳瓦尔宝典》,《技术人的管理之路》等。

年初的计划是读到的好书要写读后感记录,然后写了两篇后面就半途而弃了。写文章对于我来说还是一个比较耗时的事情,写读后感就更是如此了。一本好书,通常断断续续要个把月才能看完,看完之后内容就忘得七七八八了,需要花时间来整理,写读书笔记,在整理成文章是更加耗时的。因此,写了两篇就没写了。另外,这也可以说明做事容易三分钟热度,读书笔记和博客在下半年都荒废了。

生活

健身这一块,去年因为摔伤一次加上腿部不舒服,医生不建议锻炼,之后就一直没锻炼了,甚至好了之后也就懒得去锻炼了。一件事情放下容易,再捡起来就不容易了😅。

在年初就开始关注各家厂商的新车,并且去看了上海车展,最后买下了一辆小鹏G6。买车之后,增加了很多的开销,也增加了很多的烦恼,包括车剐蹭到了,小区停车难,但是总体来说还是增加了很多的便利。开着车子回老家,虽然时间上更长了,但是时间安排更加自由。开者车子带着家人出去玩,也不用赶时间了,并且也可以更少的做攻略。买车之后,带着老婆小孩,出去玩了几次,总体体验不错。

游玩方面,年初去了一趟景德镇,年中去了安徽的宏村,池州的蓬莱仙洞,看到了徽派建筑,玩水玩到尽兴,大人小孩都开心。年底去了一趟福建,去了海岛,吃了海鲜,还看了福建的特色建筑。因为买了大疆Action4,所以用视频记录下来了,并且放到了B站,不过所拍摄的照片却都还没有整理,游记也都没有写。

乘风启航 图: 明明

总结

这一年相对来说还是比较平淡,很多事情还是没有坚持下来,比如写博客,持续的技术学习,健身等。在年初这个时间点还是要立一下Flag,至少保证上半年是能够坚持的,如果能够坚持到全年就更好了。换另一个角度来说,如果有可能,今年还是要在半年或者季度的时候来做总结,从而保证持续把一些事情坚持一整年。

2024年,还是要继续保持阅读,坚持写作,多多陪伴家人,多多与朋友交流。

看完评论一下吧

Passkey在Android端的应用实践

2024-01-17 23:02:30

Passkey,中文名通行密钥,他是WebAuthn的一部分,由FIDO联盟开发,可以做到让用户不使用用户名和密码,来直接验证身份。在2022年的WWDC上,Apple正式宣布了对Passkey的支持,当前10月份,google也宣布了对于passkey的支持。目前已经有一些应用支持了passkey,包括谷歌,微软,github,whatsapp等。最近在我们的Android应用上集成Passkey踩了很多的坑,简单记录一下。 Passkey

Passkey原理简介

简单来说,Passkey就是通过密钥对验证替代密码验证,原理和SSL/TLS验证类似,密钥对即公钥和私钥。用户的设备上存储的是私钥,创建的时候会将公钥发送到服务器端进行存储。验证的时候,服务端发送一段通过公钥加密的options信息,设备端使用私钥解密后回传给服务器,则能够验证成功。设备上的私钥需要验证用户的指纹、faceid或者yubikey才能使用。 Android系统需要Android 9.0以后,iOS系统需要iOS116以后才能支持,除此之外,Android设备需要登录google 账号,并且手机上有google play service才行,iOS需要开启iCloud 钥匙串。Appple 和 Google 还会通过他们的Cloud来帮助我们在多台设备之间同步同一个用户的身份验证,从而可以让我们实现同一个passkey在同一个用户的多台设备使用。 具体到Android系统,首先需要用户的手机系统在Android 9.0以上,我猜测这是因为在Android 9.0之后要求手机要有安全硬件,放到StrongBox的密钥必须是放到安全硬件中的。web用户可以在手机的chrome浏览器中使用passkey。对于Android应用则需要用户手机上安装的最新的(至少是2023年版本的)Google play service,且手机上的play services不能是中国特色的阉割版本,否则google password manager不能使用,应用也不能够使用Passkey。对于Android 14以后的系统,应用是可以使用第三方密码管理器的,不过我还没有实践,这里不做讨论。

Android应用接入

Passkey验证流程 在Android中接入Passkey其实是比较简单的,具体是有两个场景,分别是创建Passkey和验证Passkey。

准备工作

为了让Android系统能够识别我们的应用支持Passkey,需要在我们的后端服务器中配置我们的Digital Asset Links JSON文件,这个文件如果我们配置过Android的App Links支持,应该是已经有的,这个文件的路径应该为https://example.com/.well-known/assetlinks.json,配置的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[  
	{    
		"relation" : ["delegate_permission/common.handle_all_urls",      "delegate_permission/common.get_login_creds" ],
			"target" : {      
				"namespace" : "android_app", 
				"package_name" : "com.example.android",
				"sha256_cert_fingerprints" : [
				SHA_HEX_VALUE
				]
		}
	}
]

其中的relation用来指定声明的关系,handle_all_urls就是指可以让app处理所有的app links, get_login_creds指的是处理登录验证。 target是表示该声明应用到的目标,namespace指定为android应用,package_name为我们应用的包名,sha256_cert_fingerprints为应用的签名SHA256。 这个文件放到我们的服务器,需要保证访问路径,跟我们前面说到的一样。并且请求返回的Content-Type为application/json。如果我们的服务端有robots.txt文件要配置允许google去访问该文件:

User-agent : *
Allow: /.well-known/

Google为我们提供了Credential Manager 来使用Passkey,需要添加如下依赖:

1
2
3
4
dependencies {
	implementation("androidx.credentials:credentials:1.3.0-alpha01")
	implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha01")
}

上面第二个依赖,如果我们是只支持Android 14以上,且不用谷歌的密码管理器,可以不用。 Proguard文件中需要添加如下内容:

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
  *;
}

创建和验证Passkey都需要CredentialManager,创建方式如下:

1
val credentialManager = CredentialManager.create(context)

创建Passkey

创建Passkey 创建Passkey的流程如上图所示,首先需要从服务端的接口拿到一些数据,把这个作为requestJson创建CreatePublicKeyCredentialRequest去调用创建Credential,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
requestJson = requestJson,
preferImmediatelyAvailableCredentials = true/false
)
coroutineScope.launch {
	try { 
		val result = credentialManager.createCredential(
			context = activityContext,
			request = createPublicKeyCredentialRequest
		)
	} catch (e: CreateCredentialException) {
		handleFailure(e)
	}
}

上面的requestJson是我们从服务端拿到的,他应该是符合WebAuthn标准的json内容,prefImmediatelyAvailableCredentials, 如何设置为true,手机上没有可用的passkey注册提供者会直接报错,而不是看有没有混合可用的passkey。requestJson的demo如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "challenge": "abc123", //服务端随机生成的字符串,用于后续判断客户端回传,用于避免被攻击
  "rp": {   //Replay party信赖方试题,用来表示应用信息,id为域名,需要和wellknown用的域名相同
    "name": "Credential Manager example",
    "id": "credential-manager-test.example.com"
  },
  "user": {  //用户信息,id和name不能缺少,displayName是可选的
    "id": "def456",
    "name": "[email protected]",
    "displayName": "[email protected]"
  },
  "pubKeyCredParams": [  //公钥凭据支持的算法类型和密钥类型,这个在webAuthn网站上可以找到相同的文档
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000, //验证超时时间,毫秒
  "attestation": "none",
  "excludeCredentials": [ //可选项,排除的凭据,可通过这个来限制不让同一台设备设置多个passkey
    {"id": "ghi789", "type": "public-key"},
    {"id": "jkl012", "type": "public-key"}
  ],
  "authenticatorSelection": { //设置支持的类型
    "authenticatorAttachment": "platform", //platform就只支持手机内置的,若为cross-platform就可支持usb的验证,yubikey等
    "requireResidentKey": true, //设置为true,则可检测到的凭据会将用户信息存到passkey中,并可以让用户在进行身份验证时选择账号。
    "userVerification": "required" //用于设置使用设备屏幕锁定功能进行用户验证,默认是preferred,用户可以跳过,建议设置为required。 
  }
}

以上json更多的解释可以看webauthn的网站: Web Authentication: An API for accessing Public Key Credentials - Level 3 (w3c.github.io)

我们客户端调用createCredential方法后拿到的结果,类似如下:

1
2
3
4
5
6
7
8
9
{
  "id": "KEDetxZcUfinhVi6Za5nZQ", //创建的passkey的base64网址编码id,需要后端存储
  "type": "public-key", //此值始终为public-key,不过ios手机上可能为passkey
  "rawId": "KEDetxZcUfinhVi6Za5nZQ", 
  "response": {
    "clientDataJSON":   "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9", //ArrayBuffer编码的客户端数据
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A" //arraybuffer编码的证明对象,包含rpid,标志,公钥等
  }
}

我们需要将这个json回传给服务器端,服务器会从其中拿到公钥,并检查其中的数据跟服务端之前给客户端的challenge是否相同,相同后会将公钥,id,与用户id对应保存起来。

验证Passkey

验证Passkey 使用Passkey进行身份验证,首先也是需要从服务端拿一些信息,如下:

1
2
3
4
5
6
7
{
  "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",   //服务端生成防止被重现攻击,跟创建流程中的一样
  "allowCredentials": [], //允许的凭证,比如只允许当前设备之前创建的凭证
  "timeout": 1800000,
  "userVerification": "required",
  "rpId": "credential-manager-app-test.glitch.me"  //信任实体Id
}

客户端使用这个json来进行验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(requestJson = requestJson)
val getCredRequest = GetCredentialRequest(listOf(getPublicKeyCredentialOption))
coroutineScope.launch {
 try {
	 val result = credentialManager.getCredential(context = activityContext, request = getCredRequest)
	 handleSignIn(result)
 } catch (e: GetCredentialException) {
	 handleFailure(e)
 }
}

google play service的凭据提供者会找到与rpid匹配的凭据,并且弹窗让用户选择,如果我们设置了allowCredentials并且只有一条会直接弹出指纹或生物验证,成功后会返回类似如下信息给我们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
    "signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
    "userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
  }
}

回传以上信息给服务端,服务端会通过存储的公钥来验证signature,能验证则说明是匹配的用户,验证通过。

踩坑分享与总结

从上面的接入代码可以看到,google 的credentials libary已经帮我们把大部分的工作做掉了,我们更多的主要是去除了requestJson中的一些参数,客户端的代码还是比较简单的。当然也遇到很多的坑。 首先就是一定要保证服务端的返回json是要符合协议的,比如authenticatorSelection要按照格式来写,pubKeyCredParams最好把常见的支持的都加上,rp信息中的的name和id一定要有,id一定要用域名。

如果存在测试版本和线上版本包名不同,签名不同的情况,一定要分别设置好digital asset设置,如果用的是同一个域名,那么是可以在一个asset文件中设置多个app的。

因为google play services是该功能的基础,所以开发测试阶段使用的网络需要能够流畅的访问谷歌的服务。使用的测试机最好也是能够使用完整的google play services的。如果测试中出现类似下面的错误,可以去尝试升级google play services解决。

During create public key credential, fido registration failure: advy: Algorithm with COSE value -8 not supported

总体来说,如果是一个出海应用,并且对于应用的安全性有很高的要求,passkey是一个很好的解决方案。但是对于一个中国的应用来说,目前passkey还是不可用的,如果使用类似的公钥-私钥验证机制,那可以使用FIDO来实现。当然因为不是像google 和apple这样对于passkey支持这么好,实现起来会更加复杂,以后有机会可以再写一写。

参考资料

看完评论一下吧

记解决MaterialButton背景颜色与设置值不同

2024-01-04 22:02:30

最近的开发过程中,因为设计的风格采用了Android的Material风格,因此我们在项目开发过程中也使用了Android 的Material组件和主题,但是开发过程中法使用MaterialButton的时候,我们给按钮设置的背景颜色和实际展示的背景颜色不一样。网上搜索了一番也没找到原因,于是便开始查阅MateriButton的代码。

期望的背景 实际的背景

经过一番研读终于找到原因,最终通过在style文件中添加如下设置解决。

1
<item name="elevationOverlayEnabled">false</item>

MaterialButton介绍

Google在Material库中给我们提供了MaterialButton组件,我们可以通过设置很多属性来设置它的样式,仅仅背景就可以设置它的边框,背景的圆角,背景的颜色,甚至可以自己设置背景的形状,除此之外还能设置文字样式,按钮上的图标等等。因为我们今天的主题是关于背景的问题的,这里我们仅仅介绍背景设置相关的东西。

正常情况下,对于一个Android的View我们可以通过设置setBackground() setBackgroundColor() setBackgroundResource()等方法来设置View的背景,而MaterialButton为了让我们能够直接修改颜色,设置圆角,则重写了setBackgroundColor()方法,实现如下:

1
2
3
4
5
6
7
8
public void setBackgroundColor(@ColorInt int color) {  
  if (isUsingOriginalBackground()) {  
    materialButtonHelper.setBackgroundColor(color);  
  } else {  
    // If default MaterialButton background has been overwritten, we will let View handle  
    // setting the background color.    super.setBackgroundColor(color);  
  }  
}

通过查阅代码,我们可以看到MaterialButton内部通过MaterialButtonHelper这个类来管理它的背景,如果我们没有通过setBackground给这个Button设置一个背景Drawable,MaterialButtonHelper会帮我们创建一个Drawable,当我们调用 setBackgroundColor的时候,实际上也会在MaterialButtonHelper内部处理,至于MaterialButtonHelper如何创建BackgroundDrawabale的流程,可以自行去看源码。

修改背景颜色的具体过程

上面说到的MaterialButtonHelper创建的背景Drawable就是一个MaterialShapeDrawable,前面的setBackgroundColor调用后实际上会调用到MaterialShapeDrawable的setTintList()方法来修改背景的颜色,实际上就是使用了Tint来修改我们的背景,在draw方法中我们可以看到如下的代码:

1
fillPaint.setColorFilter(tintFilter);

以上代码实现来背景颜色的修改。

在setTintList()的代码中和调用的函数中,我们发现了这样一行代码它修改了tintFilter这个变量。

1
2
3
4
5
6
tintFilter =  
    calculateTintFilter(  
        drawableState.tintList,  
        drawableState.tintMode,  
        fillPaint,  
        /* requiresElevationOverlay= */ true);

此处的tintList为我们刚刚设置的颜色,tintMode默认值是SRC_IN均不为空,该方法内部又调用了calculateTintColorTintFilter()方法。

到这里我们总结一下,setBackgroud是通过tint来修改了背景的颜色,tint的实现其实就是使用了Android画笔的颜色混合滤镜(PorterDuffColorFilter)来实现的。

背景颜色为什么与设置的不同?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private PorterDuffColorFilter calculateTintColorTintFilter(  
    @NonNull ColorStateList tintList,  
    @NonNull PorterDuff.Mode tintMode,  
    boolean requiresElevationOverlay) {  
  int tintColor = tintList.getColorForState(getState(), Color.TRANSPARENT);  
  if (requiresElevationOverlay) {  
    tintColor = compositeElevationOverlayIfNeeded(tintColor);  
  }  
  resolvedTintColor = tintColor;  
  return new PorterDuffColorFilter(tintColor, tintMode);  
}

以上是calculateTintColorTintFilter方法的代码,我们知道requiresElevationOverlay总是true,那就一定会执行到compositeElevationOverlayIfNeeded,那就说明这个方法内部把我们的颜色修改了,查看其实现,果然如此,代码如下:

1
2
3
4
5
6
7
protected int compositeElevationOverlayIfNeeded(@ColorInt int backgroundColor) {  
  float elevation = getZ() + getParentAbsoluteElevation();  
  return drawableState.elevationOverlayProvider != null  
      ? drawableState.elevationOverlayProvider.compositeOverlayIfNeeded(  
          backgroundColor, elevation)  
      : backgroundColor;  
}

翻阅代码我们可以看到drawableState是在Drawable创建的时候就创建了,而elevationOverlayProvider则是在MaterialButtonHelper中调用drawable的initializeElevationOverlay方法来初始化的,为ElevationOverlayProvider,而正是它的compsiteOverlayIfNeeded方法来变化了颜色。

修改颜色为我们设置的颜色

看到这里,首先想到是吧这个elevationOverlayProvider设置为null,那我们不就不会调用这个方法,颜色也就是我们最初设置的颜色了吗。然而,我们没法在MaterialButton中或者它的子类中去拿到Drawable的DrawableState,因此只能作罢。

再来继续看,ElevationOverlayProvider的compositeOverlayIfNeeded方法,它既然有个IfNeeded,那看来也不是一定会改变颜色了,继续看它的实现。

1
2
3
4
5
6
7
public int compositeOverlayIfNeeded(@ColorInt int backgroundColor, float elevation) {  
  if (elevationOverlayEnabled && isThemeSurfaceColor(backgroundColor)) {  
    return compositeOverlay(backgroundColor, elevation);  
  } else {  
    return backgroundColor;  
  }  
}

可以看到满足elevationOverlayEnabled且 backgroudColor和themSurfaceColor相同的情况下才会改变颜色,原来我设置的按钮颜色和主题中设置的colorSurface相同,此处我不可能去修改按钮颜色,我们只能去看看elevationOverlayEnabled能否修改,查看ElevationOverlayProvider的源码可以看到初始化的时候通过如下代码初始化了该值。

MaterialAttributes.resolveBoolean(context, R.attr.elevationOverlayEnabled, false)

看到这里,我们也就知道该如何解决我们的问题了,也就是在AppTheme或者当前Activity的Theme中修改elevationOverlayEnabled 为false。

除了Button之外,Material的其他一些组件也有同样使用这个属性来设置是否修改颜色的,遇到的时候也可以同样的方式解决。

看完评论一下吧

与自我和解-读《被讨厌的勇气》

2023-04-13 22:30:52

由于有了娃之后,对于小孩的管教很头大,先是看了《正面管教》这本书,书中作者的很多思想源于阿德勒的个体心理学,又看很多人推荐这本 《被讨厌的勇气》也便在微信读书将其读了。

整本书通过青年和哲人对话的方式,向我们讲述了阿德勒心理学 的内容,并且通过青年的问题开始,引导我们步步深入,最后得出人生很简单,不存在普遍性的人生意义,即使被讨厌自己的人讨厌,也可以自由地生活 。行文流畅,环环相扣,引得我很快就看完了。

决定我们自身的不是过去的经历,而是我们赋予经历的意义。这和弗洛伊德的主张相反 ,阿德勒反对使用过去的经历来解释当前的痛苦。我不敢说阿德勒的说法是否正确,但是这种方式,至少可以让自己从过去的经历中解脱,并且赋予自己生活新的意义。

我们应当尽量让自己脱离竞争的怪圈,这样自己眼中的世界会更加不同。例如,我自己之前有段时间听到扬声器中的声音,感觉很不好听,苦恼了很久,然后询问家人和朋友,他们表示其实并未关注我的声音是否好听,这些其实都是自己无意中在和别人比较竞争。

阿德勒心理的一个基本概念是,人的烦恼皆来自人际关系,有些人会讨厌自己,只是在通过自我厌弃来逃避人际关系。每个人都有自己的人生三大课题:交友、工作、爱,我们应当将自己的课题与其他人的课题分离开。比如对待儿童,我们应当将其当作一个与自己一样的一个人来真诚对待,这样可以减少其自认弱势的情绪。对于父母来说,孩子的人生也不是自己的人生,孩子的课题需要他们自己完成,父母也有自己的课题,应当避免失去自我。

对于孩子,阿德勒也并不是推崇放任,阿德勒心理学主张在了解孩子干什么的基础上对其加以守护。对于学习而言,告诉孩子这是他自己的事情,在他想学习的时候父母可以提供帮助,但是绝对不 妄加干涉,孩子没有求助的时候不指手划脚。伸伸手可以触及,但又不踏入对方领域,保持这样适度的距离很重要。只要这样,才能让孩子尽早的直面困难,学不会直面困难的孩子将会想要逃避一切困难。

跟正面管教书中的介绍一样,本书同样提到,对于孩子的教育,不可以批评也不可以表扬,并且分析了原因。表扬这种行为含有“有能力者对没有能力者所做的评价”这方面的特点,一个人表扬他人 的目的是"操纵比自己能力低的对方"。这种赏罚式的教育,对于孩子是操纵。对于孩子来说,会形成想要被别人表扬或想要去表扬别人,这种人际关系是不平等的,阿德勒称其为“纵向关系”,阿德勒心理学提倡平等的“横向关系”。自卑感就是纵向关系中产生的一种意识,如果能够对所有人都 建立起“虽不同但平等”的横向关系,就不会产生自卑情结。

不能表扬也不能批评,但是父母可以鼓励孩子。鼓励是帮助孩子用自己的力量去解决问题,需要他自己直面课题,也许要他自己下定解决问题的决心。这种情况下,双方是平等的横向关系。使用“谢谢”,对于帮助了自己的小孩或者同伴表示感谢,用“我很高兴”之类的话来传达自己真实的喜悦,也是基于横向关系的鼓励。这样做,可以让人感觉到自己有价值,从而获得勇气。

不将自己的孩子跟任何人相比,就把他当作他自己,对他的存在心怀喜悦与感激,不要按照理想形象去扣分。这样的话,平时就不会因为比别的孩子成绩差,比别的孩子淘气而烦恼了。这是所谓的不要用行为标准,而是用存在标准看待他人,不要用他人做了什么去判断,应对其存在本身表示喜悦和感谢。

同时,做人要有甘于平凡的勇气,这样对于世界的看法也会截然不同。对于自己来说 ,会减少与其他人的竞争。对于他人来说,更容易建立平等的横向 关系。接纳自己,诚实地接受做不到的自己,然后 尽量朝着能够做到 的方向努力,不对自己撒谎。

这些是对本书的一些个人总结和体会,以及部分借鉴。书的观点对于我来说很新颖,有助于从不同的角度思考人生,人际关系。下一步要继续去读一读阿德勒本书的经典书目,《阿德勒心理学》、《自卑与超越》、《理解人性》。

最后,以书中的一些句子来结尾。

人生的连续、是连续的刹那。人生很简单,并不是什么深刻的事情。如果认真过好了每一个刹那,就 没有什么必要令其过于深刻。人生中最大的谎言就是不活在“此时此刻”。起决定性的既不是明天也不是昨天,而是此时此刻。

看完评论一下吧

防御型投资者的投资基础-读《聪明的投资者》

2023-03-21 21:30:52

《聪明的投资者》这本书在微信读书的各个投资书单书单中都有推荐,在看有知有行的《投资第一课》中也多次提到这本书的内容。便找来花了一点时间读完了,读完之后觉得还是要写点东西方能将书本内容消化吸收。

我所读的是第四版的巴菲特注疏版,这本书于1949第一次出版,最后一版本即为1972年的第四版,巴菲特于2003年左右(未查到具体时间)进行注疏。书中虽然有些案列过时,一些市场的趋势有所改变,但仍然不失为一部经典。

巴菲特说,成功投资的关键是要有一个稳妥的知识体系作为决策基础,并且有能力控制自己的情绪,使其不会对这种体系造成侵蚀。这本书就是就是帮助读者构建一套自己的投资知识体系。

投资操作应该是以深入分析为基础,确保本金安全 ,并且获得适当的回报。不满足这些要求的操作就是投机。因此核心是: 深入分析,本金安全,适当的回报。同时如果想要投机操作,那么最好单独开立投机账号,并且在操作思想思路上两个账号都要完全独立开来。

作者把投资者分成两类,防御型(或者被动型)投资者和激进型(或进取型)。防御型投资者是关心资金安全同时又想花太多时间和精力的人,此类投资者的首要目的是避免重大错误或损失,其次是不必付出太多的努力、承受太大的烦恼去经常性地做出投资决策。相反,激进型的投资者则愿意付出大量的时间和精力去挑选更具吸引力的股票,以获取超出平均水平的回报。对于我们普通人来说,除了每日繁忙的工作以外还有家庭和社交,因此能够放在打理钱上的时间是非常有限的,所以大部分都还是属于防御型投资者。

投资是为了让自己的本金增值,存到银行本身就是增值,然而由于国内经济的飞速发展,各国银行不断的印钱,市场上的钱越来越多,导致持续的通货膨胀,紧靠银行存款很难跑赢通货膨胀。我们需要借助各种金融 工具来进行通知。作者建议防御型投资者同时投资和债券,并且投资于股票的资金不低于总金额的25%,但不得高于75%,同时剩下的金额投资于债券。在“熊市”股价低廉的时候,可以增加股票的投资比例,在市场价格上升到危险高度时,则应该将股票投资的比例减到50%一下。股票相比债券,可以在很大程度上上使投资者免受通货膨胀的损失,另一方面可以提供更高的多年平均回报。债券相比股票,波动比较小。对于能承受波动的年轻人,可以股票的比例高一点。债券也是有很多种,像国债,储蓄债,他们的风险较低,但同时又比储蓄的利息更高。而像企业债券,信用债,可转债等者风险要高一些,同时利息也会更高。

对于股票的购买,作者建议:

  1. 适当但不过过度分散,应该持股10~30个之间。
  2. 购买的公司应该是大型知名的 ,并且有稳健的财务。对于工业企业,股票的账面价值不低于总资本的一半,才算得上是 财务稳健的。
  3. 每家公司都有长期支付股息的历史。
  4. 应该将其买入的股票价格限制在一定的市盈率范围,参考每股收益,取过去七年的平均数,避免买的太贵。

但是通常单只股票通常都有购买限制,所以很多人的资金很难同时买到10个以上的股票。因此我们可以选择通过购买基金来间接的购买股票。在点评中,巴菲特推荐我们购买指数基金,此类基金管理费用低,同时他通常是跟踪某一市场指数,减少了基金经理人为操作的影响因素 。

投资是个人的事,应该建立在个人的独立思考和判断之上。关于投资顾问,作者认为聪明的投资者不会完全依赖金融公司提供的建议来从事买卖交易。金融服务公司的作用应当是提供建议和信息的。

最后一章,作者介绍了投资中心思想的“安全边际”,这应该是稳健投资的秘密。对于债券 ,可以通过比较企业的总价值和债务规模来计算,总价值高于债务的部分,是“缓冲价值”,对于投资者来说,在遭受亏损之前是有这些下降的空间的。同时,安全边际还能保证投资者不必对未来做 准确的预测。对于股票来说,当股价低于公司以财产和盈利为稳固基础发行的债券的价值时,可以认为有很大的安全边际。安全边际与分散化原则有着密切的逻辑联系。安全边际保证盈利的机会大于亏损的机会,分散购买保证总体亏损的可能性更小。 投资的意义不在于所赚的钱比一般人要多,而在于所赚的钱足以满足自己的需要。不确定性是投资领域最基本和无法摆脱的条件,通过学习来构建自己的知识体系就是降低不确定性,提高投资的成功率。

后记

这是离开学校之后的第一篇读书笔记,写下来是为了让自己对书的内容能够有所吸收,同时也是写作的锻炼。本文内容有不少从书中摘抄的,同时内容也有不少是个人观点,不构成投资建议。

这是一本好书,读了一遍,又画了一些笔记,写读书笔记的时候又翻了一下,还是很多没有理解,仍然需要有时间再重新来读一读。

看完评论一下吧

新版Android Studio Logcat view使用简明教程

2023-02-23 23:10:52

logcat-window.png

从Android Studio Dophin开始,Android Studio中的默认展示了新版的logcat。新版的logcat色彩上是更加的好看了,不同的tag会有不同的颜色,不同level等级的log默认也有不同的颜色。log过滤修改的更简洁了,当然使用起来也更加复杂了。原先的log视图只需要勾选就可以选择不同level的log了,只需要选择只展示当前应用的log就可以过滤掉其他应用的log了,但是新版只提供了一个输入框去过滤。在经过几个月的适应和对于官方文档的学习后,终于使用了,这里简单记录和分享一下。

定义自己专属的log view

log view 默认提供了两种视图,Standard View 和Compat View。Stand View会展示每一条log的日期,时间,进程线程id,tag,包名,log level以及message。Compat View只展示时间,log level和详细的message。可以通过log view左边的Configure Logcat Formatting Options按钮来修改,同时这个按钮中还有一个Modify Views选项可以来修改standard和 Compat视图的具体展示内容,可以定制自己的logview样式,如下图所示。

logcat-view-setting.jpg

个性化的logcat 视图不仅仅是可以自定义展示的内容,还可以修改log和filter的配色方案。前往Settings(Windows)/Preferences(Mac) ->Editor -> Color Scheme,选择Android Logcat即可修改log 的颜色,选择Logcat Filter即可修改filter的颜色。

以上修改的是logcat view的外表,我们还可以修改它的内核,一个是logcat循环滚动区的大小,以及新logcat window的默认filter,可以通过前往Settings(Windows)/Preferences(Mac) -> Tools -> Logcat 设置。

一些操作技巧

在标准布局下,或者我们的log太长的时候,一屏通常展示不下,我们需要不停的向右滑动,滚动才能看到log的信息,我们可以用log view左侧的Soft-Wrap logcat-soft-wrap.png 按钮来让log换行。

左侧的Clear Logcat按钮可以清空logcat。左侧的Pause按钮可以暂停logcat的输出,方便看错误日志,可以避免关心的日志被新的日志冲掉。

新版本中,可以通过点击logcat tab右侧的New tab logcat-new-tab.png 按钮来同时创建多个logcat view窗口。这种方式创建的不能同时展示,而利用logcat view左侧的split Panels 按钮则可以创建多个窗口,并且同时展示。每一个窗口都可以设置自己要展示的连接设备,展示样式,以及过滤选项。这样就可以很方便的同时观察多种log。

logcat-multi-window.jpg

通过键值对来过滤Log

logcat-query-suggestions.png

新的过滤器,看起来简单,实际上更加复杂且强大了。通过Ctrl+Space按键可以查看系统建议的一些查询列表。这里介绍一下查询中会用到的键:

这么多的键匹配,是可以逻辑组合的。我们可以使用&|以及圆括号,系统会强制执行常规的运算符优先级。level:ERROR | tag:foo & package:mine 会被强转为level:ERROR | (tag:foo & package:mine ) 。如果我们没有填写逻辑运算符,查询语言会将多个具有相同键的非否定过滤视为OR,其他过滤视为AND。 如: tag:fa tag:ba package:mine 计算逻辑是 (tag:fa | tag:ba) & package:minetag:fa -tag:ba package:mine 计算逻辑是 tag:fa & -tag:ba & package:mine。这里的-用来表示否定,既tag不包含ba的情况。

新版的logcat view当然也是支持正则的,tag、message、package、process这几项是支持正则的。使用正则需要在键后面加一个~,例如: tag~:My.*Report。 除了正则这个选项之外,这几个键还有完全匹配和包含字符串即可的选项。不加修饰符号就是包含指定的字符串即可匹配。如果后面加=则要完全匹配才可以,例如process=:system_serverprocess:system_ser可以匹配到system_server的log,但是process=:system_ser则无法匹配到。

同时如上几个匹配选项都支持和前面说的否定符号连用如:-process=:system_server

既然新版支持了这么复杂和强大过滤功能,如果每次都现想现写,那肯定是头皮发麻。as也为我们提供了收藏和历史记录功能。点击右侧的的星星按钮即可收藏当前的过滤条件,点击左侧的漏斗即可查看历史和收藏,并且可以删除不想要的记录。

切换回旧版log view

最后的最后,如果你觉得新版本适应不了,还是想要切换回旧版本的log view,还想要保留新版的android studio,也还是可以通过修改设置进行切换的。 前往Settings(Windows)/Preferences(Mac) -> Experimental, 反选Enable new logcat tool window 即可,如下图所示。

disable_new_logview.jpg

学习工具的目的,是为了让工具更好的为我们服务。希望大家都能够通过使用as提供的新功能来提高效率,从而有更多的时间去风花雪月。

参考:https://developer.android.com/studio/debug/logcat

看完评论一下吧

景德镇一日匆匆游

2023-02-04 01:18:57

老婆家在皖赣交界的县,自驾只需要两个多小时就能到达景德镇,因此,很早就有想要自驾前往的想法,由于自己没有车因此一直没能成行。

今年过完年过来老婆家,一时兴起,决定租车前往。

作为驾驶员,一路上穿行在高速上,且经过很多的山和隧道,没什么机会拍照留下影像,主要是到达景德镇拍了一些照片。

恰逢周六,景德镇每周的乐天陶社创意集市在12点会收摊,我们首先驱车来到这里,人很多,各种摊位很多陶瓷很有意思,因此集市也没有留下什么照片。旁边的店铺里面有很多各种各样的陶瓷玩意,买了几个陶瓷玩具给小孩玩。

在旁边的一个不起眼的巷子中发现了一些师傅,有的在做模,有的在做坯子,甚至有些我也看不懂是什么步骤,但是陶瓷器具很多都要经过这些步骤吧。

随后便去了网上知名的寻味三宝吃饭,这家店所在的路两边都是山,如果有时间多多停留转转应该很不错。店内装修风格独特,但餐品就是比较常见了,点了几只烤乳鸽小家伙们倒是喜欢的很。

吃完饭,便前往御窑遗址博物馆,公园内的塔只能一楼看一下。遗址内可以看到明清时候的一些遗迹,博物馆的红砖建筑很漂亮,很多人在此处拍照。御窑博物馆内令人印象认可的是青花瓷特展,青花瓷真的非常漂亮,光线原因,没有能够拿得出手的照片。

博物馆的周围有很多古民居,但是都在进行修缮,相信下次再来的时候应该会很热闹了。旁边有一些非遗传承人,有人在售卖陶瓷,也有人在制作陶瓷,这些手工作品确实是工业机器很难制造出来的。

之后去了抚州弄小吃街,想象中的小吃街应该熙熙攘攘,然而到了之后发现街上冷冷清清。除了牛骨粉和油条麻糍,其他的也算不上特色了。买了两个油条麻糍便决定撤了。

最后去了中国陶瓷博物馆,其中看到了中国各个时代的陶瓷,从唐宋到现代,对于了解中国陶瓷的发展收获很多。另外还有一些特别展览,也很不错。由于时间原因,并未能全部看完。

由于总共花了五个小时左右的时间在路上,并且又带了三个娃,此次行程是非常紧凑,又非常匆忙的。博物馆的展都只是走马观花的看了一下,很多地方也没能够去,甚至也没能够去淘一套好用的茶具回来。

留有很多遗憾,希望下次有机会再回来好好看看。

看完评论一下吧

面试官谈面试

2023-01-16 23:30:52

 图: 明明 参加工作多年,找工作时参加过一些面试,经历过笔试,面试,电话面试等多种形式 。也作为面试官面试过很多人,也参加过公司的面试培训,观摩过其他同事的面试。最近系统学习了一下极客时间的《技术面试官识人手册》,课程介绍的东西虽然不能全盘拿来使用,但是还是有很多东西指的借鉴,今天想要从面试官的角度分享一下个人的学习体会以及个人对于面试的一些见解。

面试其实是一个公司与候选人进行双向交流的机会,也是双方进行双向选择的过程,公司通过面试官向候选人展示公司员工的风采以及介绍公司的业务技术等,候选人则是通过面试向面试官展示自己的技术素养,业务能力等。同时,面试的过程也是面试官和 候选人进行双向学习的机会。

公司和候选人双方都需要几轮面试过程中互相评估对方是否和自己匹配,公司需要确认候选人是否具有公司所需的技术能力(包括相关岗位必须的知识能力,软件编程能力,系统设计能力等),业务能力(对产品较好的理解能力,项目管理能力等),个人的综合软素质(学习能力,沟通能力,好奇心等)。

通常的招聘理念是招聘优秀的工程师而不仅仅是能干活的人,优秀的人常常是在他自己领域的技术知识是扎实的,能够把具体问题抽象成可解决的软件问题,并且能够使用软件工程的知识与技能去解决和实现他们。同时他有很好的代码设计实现能力和系统设计能力。在非技术层面 ,他能够有用很好的上面说到的一些软素质能力。

对于不同工作年限的人,面试的侧重点也会不同。对于刚毕业的学生或者工作一两年的新人,会更加关注他的成长潜力以及相关方面的软素质,比如会通过他以往的学习或者项目经历来考察他是否具有足够的学习能力,是否能够很好与人合作,用过的技术是否掌握扎实,有没有对于技术方面的热爱等等。对于一个经验丰富的老兵,通常会关注他的视野,以及他对于技术理解的深入,对于不同的团队来说,可能还会关注对于产品和团队的理解,软件系统的理解等。同时,不同的团队对于人的要求也是不一样的,有些团队也会寻找某些方面与团队比较互补的人选。

面试形式一般就是两人的对聊或者白板编程,想要对候选人进行评估 ,面试题的设计其实就很重要。好的面试题,应该是区分度、深度和覆盖范围都很好的。因此面试的过程当中,面试官可以根据候选人的表现来调整题目的难度,一是在候选人答不出来的时候给提示或者弱化要求来降低难度,二是可以通过追问和附加更多条件来增加难度,加大对于深度的延伸。

好的编程题或者系统设计题目,一般不会直接给一个抽象的问题(比如让候选人写出二叉树的前序遍历),而是一个实际问题。这样的问题,不仅仅可以考察编程能力,还可以对于实际问题的抽象能力以及需求分析的能力,同时面试官还可以观察候选人的沟通能力。对于这类问题,也便于面试官进行难度的调整。对于候选人,也应该多和面试官进行沟通,确认自己的理解是否正确,避免后续编程的实现完全错误。

对于软素质方面的考察 ,一般会通过面试的过程中进行考察,比如通过编程环节考察沟通能力 ,需求分析能力。以及会通过与候选人聊过往项目和经历来考察,因为一个人过往环境中解决困难问题时候所采取的行为在相当程度上决定了他未来会 怎样应对类似的问题。

好的面试官会有很好的面试礼节,在面试开始会握手问好,帮助候选人化解紧张气氛,进入到面试中。面试结束也会留给对方问问题的机会,并对于候选人表示感谢等。

最后再说一下,公司的面试通常不是为了把候选人考倒,而是发现对方的亮点和长处,能够为自己找到合适的人才。

2023年的第一篇文章,是个人写作能力的锻炼,也是输出的尝试,毕竟只有能够输出才算对所学内容有所消化。

看完评论一下吧

2022年个人总结

2023-01-03 21:30:52

向上生长 图: 明明 已经有几个年头没有写过年终总结了。 2022年是非凡的一年,看到很多人写了年终总结,忍不住也来写一写。 这一年如果使用几个关键词来总结,我想我的关键词是魔幻,起伏和充实。

魔幻

2022年已经是疫情的第三年了,反而过得比疫情的第一年更加的艰难。这一年,我们经历了两个月的全城封闭,大规模的参与团购。这一年我们开始每天做核酸,没有核酸你就寸步难行。在解封之后,因为次密接提级,被拉到了方舱隔离了一周,没有网络,使用自己的手机流量联网办公。在这一年即将结束时,我们得以全民放开管制,由于政策的突变,大量的人感染,并且医疗和药品准备不足,很多人买不到要,或者无法就医。当然了,在这一年的年尾,我也全家感染新冠,所幸大家都康复了,为这一年画上了完美的句号。疫情之魔幻,难以捉摸。

起伏

一方面,起伏是 工作上的起伏。三月封闭前几天,公司领导宣布了公司运行出现了问题,团队即将解散。公司从去年下半年开始各方面情况就不太好,过完年回来之后其实公司在产品迭代,和运行上就没有什么动作了。公司同事们这段时间也都是摸鱼的状态,这个消息对所有人来说都是非常突然。几天之后,大家就都被关在家里了,也就只好各自准备开始面试。我们这样一个小的创业团队,也即就此解散,由于疫情原因很多人都没能在见面告别。 之后一段时间便是紧张的复习准备面试。对于我在今年找工作真的是非常困难,一方面是整体大环境不好,很多公司缩减招聘名额,另一方面是由于前前公司的竞业限制仍未到期,很多的互联网公司都不能投递。经过一个多月的努力,终于找到了一份满意的工作,具体公司不便透露。相比与之前几份工作,新公司是全新的行业,很多行业知识都不了解。之后的一段时间便是开始在新公司熟悉业务,熟悉同事。同样由于疫情原因,到了八月份才 得以去导板公司与同事们进行交流。

另一方面是个人心态和情绪的起伏。在年初回来工作初期的阶段,是比较忐忑的,知道公司状况不太好,担心可能某天会面临公司破产,同时又希望这一天不会到来。在被告知团队解散后,一是惋惜团队最终还是创业失败了,另外也比较担心自己的前途。找工作期间,则是比较焦虑,一方面市场不好,二是许久都没有参加过什么面试,另外比较担心自己和更加年轻的工程师相比是否竞争力不足。在此,要感谢新公司同事在面试和工作过程中对我的认可和支持。入职新公司之后,心态上面调整了很多,但是心有余悸,仍觉得需要未雨绸缪,所以还是花了时间充实自己。

充实

平心而说,从四月算起的这一年,还是特别的充实的。 首先,疫情封闭起就报名了英语培训课程,这一次没有半途而废,坚持了下来,完成了最多100多天的每日学习签到。到目前为止,虽然英语还不是很好,但是已经能够借助google meeting的实时字幕功能,与外国人进行交流,并且比较顺利的与几位海外候选人进行英文面试,与海外同事进行需求沟通。 另外今年耐心看了很多不错的好书,即使解封后每天有很多工作要做,睡前也会抽一点时间进行阅读。记忆犹新的好书有邓小平时代,周恩来传,置身事内,正面管教等。伟人对于个人的工作其实没有什么用,但是通过他们的经历他们所作的决策令人敬佩,鼓舞自我。置身事内这本书,让我对于中国当前的经济政策有更详细的了解,知道了一些政策的制定的原因,比如中国的分税制,城市的土地政策等,也让身在国内的我们对于国内经济的走势有了一点点的理解。 最后就是,在8月份开始,重新开始去锻炼身体,并且今年坚持的还不错,每周给自己定下一些简单的训练计划。多年以前开始健身,中间一次又一次的开始之后又是放弃。这一次希望能够坚持下去,打造更加健康的身体,同时也能够在体型上有所改善。

总结

以上就是2022年的大体情况,对于个人来说,其实这一年也还不错,但是焦虑依旧在。新的一年,打算立个Flag,尝试进行一些输出,让这个荒废已久的博客增加一点生机。在过去一年,虽然有比较多的输入,读书或者看一些专栏之类的,但是却没有输出,看的书基本上都是泛读,知识的消化还是很少。新的一年,可以写一些读书笔记,将个人的输入转化输出,提高自我的写作和表达能力。 新的一年要多与朋友在理财,个人学习,读书等方面进行交流,如果有可能去认识更多的朋友,也欢迎读者与我交流。当前在做的一些事,比如健身,英语学习等,仍然要持续的坚持下去。

看完评论一下吧

2022年在MacOs上编译AOSP

2022-05-17 22:30:52

之前苦于电脑磁盘空间比较小,而android系统的源码越来越大,一直没有机会自己编译Android系统。这次换了电脑,磁盘足够大,可以尝试一下了。 而android源码的网站红色的字写着 Platform development on MacOS isn’t supported as of June 22, 2021. ,我就知道不会那么容易了。

我的电脑环境:

2021款 M1芯片 Macbook Pro 16GB运行内存 MacOS Monterey(12.1)

环境准备

安装xcode和相关工具

$ xcode-select –install

$ softwareupdate –install-rosetta

创建大小写敏感的磁盘映像

我们先创建个350GB的大小

$ hdiutil create -type SPARSE -fs ‘Case-sensitive Journaled HFS+’ -size 350g ~/forest.dmg

设置环境变量

我本地用的zsh,直接在.zsh_env中写,并配置挂载映像的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#  设置最大打开文件数量,防止编译过程中因为打开文件数量太多失败
ulimit -S -n 2048
# 编译缓存
export USE_CCACHE=1
# 挂载映像
function mountForest { hdiutil attach ~/forest.dmg.sparseimage -mountpoint /Volumes/Forest; }
#卸下挂载
function umountForest() { hdiutil detach /Volumes/Forest; }
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
export PATH=~/.bin:$PATH

编辑完保存之后,执行一下如下语句使配置当前就能生效 source ~/.zshenv

下载源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ mkdir ~/.bin
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo #下载repo
$ chmod a+x ~/.bin/repo #设置repo权限
$ mountForest #挂载映像
$ cd /Volumes/Forest
$ mkdir aosp_mata
$ cd aosp_mata
$ git config --global user.name "Your Name"
$ git config --global user.email "[email protected]" 

$ repo init -u https://android.googlesource.com/platform/manifest -b android-11.0.0_r48 #可自选版本,我这用的是11.0.0的最后一个tag
$ repo sync

之后便是无尽的等待去下载源码,国内的网络下载不了,自己想办法爬墙吧。

开始编译

如果没有问题,在源码目录直接执行以下命令就可以编译了

1
2
3
$ source build/envsetup.sh
$ lunch aosp_arm-eng
$ make -j4

lunch后面的参数也可以不填,则会显示出来所有可以的选项,自己选一个进行设置就行。 make就开始编译, -jN用于设置任务数, N应该介于计算机上的CPU线程数的1-2倍之间为宜,我的M1 MAC 是10核,就先设置了24。

理论上这样就可以慢慢的等就能编译成功了,然而,如果可以这么简单就不需要我写一篇文章了,直接看android官方文档就行了。为了节省时间,先把下面的问题改改再编译。

问题解决

问题一:Could not find a supported mac sdk

大概是这样的log

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[ 94% 171/181] test android/soong/cc
FAILED: out/soong/.bootstrap/soong-cc/test/test.passed
out/soong/.bootstrap/bin/gotestrunner -p ./build/soong/cc -f out/soong/.bootstrap/soong-cc/test/test.passed 
-- out/soong/.bootstrap/soong-cc/test/test -test.short
--- FAIL: TestDefaults (10.86s)
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
    cc_test.go:3075: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
--- FAIL: TestDoubleLoadbleDep (0.05s)
    cc_test.go:733: "Could not find a supported mac sdk: [\"10.10\" \"10.11\" \"10.12\" \"10.13\" \"10.14\" \"10.15\"]"
..... ....

原因是指不到指定的mac sdk 可以看一下/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs文件下有没有 MacOSX12.3.sdk , 然后在然后在 /build/soong/cc/config/x86_darwin_host.go 文件中找到 “darwinSupportedSdkVersions“ 添加 MacOSX12.3.sdk 对应的版本号——12.3,如果你的macosx的sdk是别的就填别的。

darwinSupportedSdkVersions = []string{
		"10.10",
		"10.11",
		"10.12",
		"10.13",
		"10.14",
		"10.15",
		"12.3",
}

另外你也可以到https://github.com/phracker/MacOSX-SDKs/releases 去下载10.15的sdk放到上面的文件夹里面。

问题二: v8引擎无法编译,一些文件找不到

由于2021年后,官方不维护mac上的开发环境了,所以external/v8下面有很多编译错误,这里直接采用回滚代码的方式,我是回滚到了 Upgrade V8 to 8.8.278.14 提交的前一个Commit

1
2
cd external/v8
git checkout 9304fbb

问题三: undeclared identifier ‘PAGE_SIZE’

1
2
3
4
5
6
system/core/base/cmsg.cpp:36:21: error: use of undeclared identifier 'PAGE_SIZE'
  if (cmsg_space >= PAGE_SIZE) {
                    ^
system/core/base/cmsg.cpp:78:21: error: use of undeclared identifier 'PAGE_SIZE'
  if (cmsg_space >= PAGE_SIZE) {
                    ^

看起来是PAGE_SIZE这个常量没定义,那就去补上呗。去 system/core/base/cmsg.cpp 文件开头添加 PAGE_SIZE 的声明

1
2
3
#ifndef PAGE_SIZE
#define PAGE_SIZE (size_t)(sysconf(_SC_PAGESIZE))
#endif

问题四: incompatible pointer types passing ‘unsigned long *’ to parameter of type ‘uint32_t *‘

external/python/cpython2/Modules/getpath.c:414:50: error: incompatible pointer types passing 'unsigned long *' to parameter of type 'uint32_t *' (aka 'unsigned int *') [-Werror,-Wincompatible-pointer-types]
else if(0 == _NSGetExecutablePath(progpath, &nsexeclength) && progpath[0] == SEP)

external/python/cpython2/Modules/getpath.c 中:

1
2
3
4
5
6
7
#ifdef __APPLE__
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
    uint32_t nsexeclength = MAXPATHLEN;
#else
    unsigned long nsexeclength = MAXPATHLEN;
#endif
#endif

改成:

1
2
3
#ifdef __APPLE__
    uint32_t nsexeclength = MAXPATHLEN;
#endif

external/python/cpython3/Modules/getpath.c 中的:

1
2
3
4
5
6
7
8
#ifdef __APPLE__
char execpath[MAXPATHLEN + 1];
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
    uint32_t nsexeclength = Py_ARRAY_LENGTH(execpath) - 1;
#else
    unsigned long  nsexeclength = Py_ARRAY_LENGTH(execpath) - 1;
#endif
#endif

改成:

1
2
3
4
#ifdef __APPLE__
char execpath[MAXPATHLEN + 1];
uint32_t nsexeclength = Py_ARRAY_LENGTH(execpath) - 1;
#endif

以上问题改完应该就可以编译成功了,如果是在后面java编译阶段失败,可以先试试重新执行make试试,如果还是不行的话就上网找解决方案吧。

编译idegen模块导入Android Studio

使用如下命令编译idegen模块:

1
mmm development/tools/idegen/

完成之后,执行如下命令:

1
development/tools/idegen/idegen.sh

之后就会在根目录生成对应的 android.iprandroid.iml IEDA工程配置文件,之后在IDEA或者Android Studio中打开android.ipr就能浏览源码了。

参考

参考了以下资料和网友的分享,非常感谢:

看完评论一下吧

印度孟买、昌迪加尔、加尔各答之行

2019-11-29 10:33:48

跟着同事一起出差,去了印度这个神奇的国度,真实的见到了火车挂人的情景和当地的一些风土人情。

当地的路上人车混行,很多载客三轮车,三轮车司机通常也不穿鞋。街边的手机店被中国手机厂商占领,在去的几个城市都有很多的vivo、oppo、xiaomi店,当然也有很多的三星店。

在车站看到不少人使用头顶着大袋的货物。

在孟买乘坐的城市铁路,听同事说是英国统治时期修建的。我们购买的高级车票,车厢人相对少一点有座位,10卢布一人。火车上有挂人,但是也没有传说的那么夸张。车子没有空调,车窗都是打开的,车子开起来后也很凉快。这张图是一张女士专门车厢。

孟买靠近印度洋附近的一处教堂。

小朋友们在出租车中玩耍,看到我拍照后很配合的做动作和鬼脸。

路边买小吃的摊贩。选好各种小吃,一起放到报纸卷好的容器里。当地上班族,中午也是在路边吃。

印度洋海边。海面上很多大大小小的各种船只。

印度洋船上工作的船员。

昌迪加尔是印度北部的一座年轻城市,城市很新,很少见到孟买一样的贫民窟。50年代才开始建城,道路和城区使用字母和数字组合分区。在当地去了一所大学和一个石头城公园,石头城公园有很多碎瓷片做的小人和小动物。

石头城公园拍到的一个抱小孩的女人。

加尔各答是最后一个行程地。当地有很多英式风格的建筑,我去参观了维多利亚纪念堂,内部改造成了历史展览馆。

参观维多利亚纪念堂的当地学生

加尔各答河上的船夫

当地的公交车

当地的有轨电车,看起来很破,轨道没有任何保护和隔离

公路高架下生活的小孩,他们养的用来表演的马匹

看完评论一下吧

美国湾区参加Google IO大会之行

2018-05-29 01:18:57

旧金山机场出来后立马去租车的地方取了车,直奔金门大桥。当天阴天,还有一点儿冷。

湾区海边的海鸟

海边的餐厅

斯坦福大学在硅谷离我们住的地方不远,第二天便过去转了一圈。学校是开放的,外面的草坪很大,学校的建筑有很多宗教特色的地方。

之后开始了此行的正事参加谷歌开发者大会,以及大会网上的表演。

参加大会最后一天结束后又去了一下,硅谷必打卡地方,计算机历史博物馆,此图是一台古老的计算机。

另外此行还参观了谷歌的办公室,谷歌Android展览区,facebook办公室,苹果展览中心,但是没有拍到自己认为很好的照片,在此不表。

看完评论一下吧

日本关西蜜月旅行

2018-04-26 01:18:46

日本关西蜜月行,去了京都、奈良,大阪三个地方。京都主要看风景,日本的一些古建筑,还有樱花的尾巴。奈良主要看小鹿。大阪去海游城和买买买。

晚上就在二条附近的餐饮街转转很有风情

准备去二年坂,出门没多久就遇到一个神社,日本神社是真的多

路两边也是有很多古建筑

还遇到了一些拍结婚照的日本夫妇

以及拍到了樱花的尾巴

日本的学生

二年坂的路上

清水寺的塔

高地俯瞰。之后又去了稻荷大社,图片放到封面啦。😄

次日又去了金阁寺,主要就是这个金阁,别的不太有意思。另外去了岚山,没有拍下好看的照片。在此不表。

在奈良呆了不到一天,主要就是看小鹿在当地的市场转了转,另外看到了摩拜单车,价格死贵,又找不到还车的地方,把我可坑惨了。

奈良的小鹿是真彪悍,都是跟游客抢食物吃的。不过也有些很好,跟游客合照。

此时的枫叶还是一片翠绿,秋天过来一定很美。

大阪的两日通卡是很推荐办理的,在地铁站就可以办。地铁免费通行,不包括JR哦。另外送了几个免费温泉,一些景点优惠券,还有免费的游船,免费的摩天轮。图上是去的大阪城天守阁。

图上是坐船或者摩天轮拍的照片,一半红色的建筑是大阪海游馆,没有好的照片PO,不过很棒,以后有娃一定要带着来参观。

最后是在道顿掘拍到的大阪博览会的宣传和可爱的熊猫。

看完评论一下吧

记录博客转HTTPS

2017-07-12 21:30:52

越来越多的网站已经支持https了,谷歌浏览器对于未使用https的网站会提示不安全。本站采用hexo来静态生成页面,之前托管在github pages上面,因为一直也没有弄。周末在家闲来无事,顺便就把博客改到vps上面,然后添加了https支持。

博客推送到vps

原先博客是推送到github pages的,其实就是传到github的仓库,要迁移到vps,其实也比较简单,在vps上面创建一个git仓库,把代码推到这个git仓库,然后在vps上启动nginx就可以了。具体真可以参考网上的文章:https://www.qcloud.com/community/article/241080001487926962添加对于HTTPS的支持nginx服务器已经支持https了,目前也很成熟了,我们可以使用免费的let’s Encrypt,国内又拍云,七牛都支持申请,但是他们审核什么的还需要时间,所以我们还是直接使用官方方式最好最快了。 Let’s Encrypt官网有详细的介绍,对于不同的系统,不同的网站服务器都有详细的指南。我这里只介绍一下基于ubuntu 16.04,nginx的安装过程。 签名的自动什么和安装是通过Certbot来执行的,我们需要安装certbot的源和软件。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx

然后再执行 sudo certbot –nginx

就可以了。在执行这个命令之后,会有一些提示,首先会提示输入邮箱地址,之后会读取nginx配置读取到域名然你选择,之后就会安装成功了。并且会自动重启nginx使https生效。 我们可以查看网站的nginx配置文件,一般在/etc/nginx/下面,暂时我的服务器上面只有一个网站因此使用默认的就好,是在 /etc/nginx/site-available/default 文件,可以看到certbot给我添加了如下内容:

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/isming.me/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/isming.me/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    } # managed by Certbot

第一句表示监听443端口,因为https默认端口是443,第二句第三句是签名的key(公钥和私钥),第四句是配置,支持哪些加密方式啊,哪些ssl的协议啊,具体可以自己去看文件。 下面判断是否为https,如果不是就重定向到https,这个也是在certbot在安装的时候会让你选择的,如果选择easy模式则没有这个,选择security模式则有。 另外,Let’s Encrypt的签名有效期只有90天,要记得在签名到期之前重新申请。certbot工具支持更新签名,调用以下命令即可:

    sudo certbot renew

同时我们可以自己配置cronjob来定时更新签名。 以上可以看到支持https,越来越简单了。 涉及到其他系统请查看官方文档:https://letsencrypt.org/getting-started/

看完评论一下吧

简单聊聊Android Architecture Componets

2017-05-18 20:01:38

Google IO大会进行中,本次大会Android最大的新闻当属Android O以及Kotlin被官方认可。我发现了原来还有发布官方的架构库,以及推荐使用指南,分享给大家。

架构原则

新架构

架构图

如上图所示,为新的架构模式:

Activity/Fragment

UI层,通常是Activity/Fragment等

监听ViewModel,当VIewModel数据更新时刷新UI

监听用户事件反馈到ViewModel。

ViewModel

持有保存,或者想Repository来获取UI层需要的数据

响应UI层的事件,执行响应的操作

响应变化,并且通知到UI层

Repository

App的完全的数据模型,ViewModel交互的对象

提供简单的数据修改和获取的接口

配合好网络层数据的更新与本地持久化数据的更新,同步等

Data Source

包含本地的数据库等,网络api等

这些基本上和现有的一些MVVM,以及Clean架构的组合比较相似,不过谷歌提供了一些新的类库来帮助我们实现这个架构。

谷歌的新玩具

本地IO大会谷歌提供了新的类库来实现这个功能,小标题我写新玩具是因为这个库目前还在alpha1版本,官方只建议在个人小项目中使用。

这个类库包含如下一些东西:

Android声明周期的回调,帮助我们将原先需要在onStart()等生命周期回调的代码可以分离到Activity或者Fragment之外。

一个数据持有类,持有数据并且这个数据可以被观察被监听,和其他Observer不同的是,它和Lifecycle是绑定的。

用于实现架构中的ViewModel,同时是与Lifecycle绑定的,使用者无需担心生命周期。方便在多个Fragment之前分享数据,比如旋转屏幕后Activity会重新create,这时候使用ViewModel可以方便使用之前的数据,不需要再次请求网络数据。

谷歌推出的一个Sqlite ORM库,不过使用起来还不错,使用注解,极大简化数据库的操作。

框架补充

​ 工具库帮助我们进行开发,如果不满足官方的库其实可以自己实现。比如LiveData在某些情况下可使用RxJava代替。

​ 数据层官方推荐使用Room或者Realm或者其他Sqlite ORM等都可以,同时从某些方面看Room风格很像Retrofit。网络请求也被推荐使用Retrofit。

​ 各层之间的耦合推荐使用服务发现(Service Locator)或者依赖注入(DI),会上推荐了Dagger。

测试

​ 各层之间的合理分层,为测试提供极大的方便。

​ 使用Android Instrumentation Test,借助Espresso库进行,借助Mock的ViewModel,可以专注于测试UI

​ 使用Mock的Repository来提供数据,使用JUnit测试,因为不涉及UI,运行速度会快很多。

​ 数据层Mock一些数据返回给Repository,使用JUnit测试即可

​ 使用JUnit测试

​ 数据库,使用Room的话官方提供了测试支持,在测试时候创建内存数据库即可。

​ 网络请求,使用MockWebServer来提供假的服务端即可。

示例

再补一个会议时的项目结构图,以一个用户信息页面为例。

最后的话

​ 目前这个库还不完善,api可能随时会变,公司项目不建议使用,个人项目可以尝鲜。另外对于已经有的项目,也不建议更换到现在的架构。不过这个项目的好的思想可以借鉴到我们自己的项目中来,同时这个库的方式我们其实可以借助其他的开源库来实现。

本文不再贴相关代码,具体各个库的使用请查看官方文档https://developer.android.com/topic/libraries/architecture/guide.html

附上官方的DEMO项目:https://github.com/googlesamples/android-architecture-components

这次的视频:https://www.youtube.com/watch?v=FrteWKKVyzI

文中如果错误,欢迎指正.

看完评论一下吧

聊聊Android N开始支持的Lambda

2016-09-13 20:39:03

Android N 正式版已经发布了。对于开发者来说一个重大的更新是对于Java支持到了Java8,其中一点就是支持Lambda。我们就来聊聊什么是lambda,怎么在Android中使用。

什么是lambda

Lambda 可以理解为匿名函数,帮助我们写出更加简洁的代码。

给view设置一个clicklistener,原本你需要写出这样的代码:

1
2
3
4
5
6
v.setOnClickListener(new View.OnClickListener(View v) {
 @Override
 public void onClick(View v) {
 Toast.makeText(getActivity(), "clicked", Toast.LENGTH_LONG).show()
 }
});

使用lambda之后:

1
.setOnClickListener(v -> Toast.makeText(getActivity(), "clicked", Toast.LENGTH_LONG).show());

是不是代码量爆减。这里再看下怎么写lambda。

在JavaScript,python等语言中函数是一等公民,但是Java中类才是。使用lambda时候,lambda其实应该是一个对象,依附于函数式接口(只包含一个抽象方法声明的接口,例如刚刚我们举例的OnClickListener就是,在Java 8 需要使用@FunctionalInterface这样保证在编译的时候一个接口只有一个抽象注解)。

写法的基本规则是这样:

1
(arguments) -> {body}

arguments 是参数列表,0~n个, 参数为一个时候,可以不要括号
body 为具体代码部分,如果代码只有一句的话可以不要大括号
返回值会自动推导出类型

一些写法实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> System.out.println(s);

() -> 42

() -> { return 3.1415 };
(a, b) -> {return a+b;}

另外一点需要注意的是,在我们的lambda表达式中this关键指的是外部对象,而不是我们以为的lambda这个对象。在语法糖的实现过程中,lambda表达式最后会被变为类的私有方法,因此可以放心的使用this。

使用retrolambda

目前有个比较成熟的解决方案,使用retrolambd,接入的配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apply plugin: 'com.android.application' 
apply plugin:'me.tatarka.retrolambda'

buildscript {
    repositories {
        mavenCentral()
    }

 dependencies {
     classpath 'me.tatarka:gradle-retrolambda:3.2.5'
 } 
}
  // Required because retrolambda is on maven central
repositories {
    mavenCentral() 
}
 android {   
     compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8   
        }
}
//指定将源码编译的级别,以下会将代码编译到1.7的自己码格式
retrolambda {
	javaVersion JavaVersion.VERSION_1_7
}

当前,retrolambda对于android gradle插件是有依赖的,需要使用1.5+的插件才可以。

retrolambda的原理是在编译的过程中,给class文件增加包裹,转成java 1.7支持的格式。

使用jack

在Android N,支持使用Java 8, google给我们提供了新的编译工具jack,因此可以直接支持lambda,为了支持低版本的Android也可以用lambda,我们需要将targetSdkVersioncompileSdkVersion设置为23或者更小。启用jack,修改build.gradle如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
android {
  ...
  defaultConfig {
    ...
    jackOptions {
      enabled true
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

jack工具链会的编译步骤如下:

Jack (.java --> .jack --> .dex)

和之前相比,将中间转换为class文件的步骤省略了,不需要多个工具链。 在低版本兼容lambda,也同样是使用的语法糖来实现。

后记

如上两种工具都可以让我们在进行Android开发的时候来使用lambda,retrolambda出来的时间更早,经过很多次的迭代,目前也有一些app在使用,相比较来说更加成熟。jack则是google开发,减少了对javac的依赖,更多谷歌的自主性,相信后面谷歌大力推广的,但是出于刚刚开发出来因此还不够成熟,对于lint,proguard,instant run还有很多地方支持不好的地方,我们相信以后jack会是趋势。

出于尝鲜,还是可以来使用的,但是在大的项目里面还是不建议使用的,毕竟万一出了问题还是很难排查的。

另外,如果想要在android开发更爽快的使用lambda,也可以去试试kotlin这个语言。

参考资料:

  1. http://viralpatel.net/blogs/Lambda-expressions-java-tutorial/
  2. https://developer.android.com/guide/platform/j8-jack.html
  3. https://github.com/evant/gradle-retrolambda

看完评论一下吧

小红书Android客户端演进之路

2016-08-08 19:13:00

小红书Android客户端第一个版本于2014年8月8日发布,转眼到了2016年8月8日,小红书Android版本发版两周年。趁机回顾一下小红书的Android版本,两年中我们踩过很多坑,收获很多经验,分享出来与大家共勉。 小红书从最初1.0到现在目前4.7版本,历经两年,安装包从原先的5M发展到现在的17M,产品模块也从原先的只有社区模块发展到了具有社区和电商两个大模块。App包含社区、电商、支付、推送、直播、统计等各种功能和模块,那么开始吧。

功能演进

两年的时间,30多个版本的迭代,许多功能都有了翻天覆地的变化。我们的新人欢迎页也是从最初的比较炫的效果发展到目前比较稳定的简洁版本。当初钟大侠花了无数个日日夜夜,苦心做出来了多个欢迎页动画,虽然现在已经不再使用,但是我们也学习到了一些新技术。后来,钟大侠还是将其贡献到了github开源社区中。 欢迎页第一版

下载地址:https://github.com/w446108264/XhsParallaxWelcome

欢迎页第二版

下载地址: https://github.com/w446108264/XhsWelcomeAnim

社区是小红书的核心价值之一,笔记是小红书社区的核心体现,毋庸置疑,笔记发布是小红书App的核心功能之一,我们一直在产品和技术上,优化我们的笔记发布流程和功能,包括我们将只支持分享单张图片,扩展到现在支持多张图片同时发布。同时支持更丰富的图片编辑效果,更加便捷的发布笔记。

小红书的笔记展现形式和大多数其他的图片社交App类似,我们也支持图上标签功能。最初小红书图上标签是同其他App类似的黑色的标签。不过在3.0之后,小红书创造了独特的树状标签,给用户带来焕然一新的体验,同时也被其他App竞相模仿。新的标签给技术也带了很多的挑战,我们重新定义了标签的结构,以及标签的生成和展示。可以查看我以前的博客,来看看我是怎样做标签的动画的。(http://blog.isming.me/2016/06/07/path-property-animation/

UI的改版,功能上的改动还有很多,这里不再一一提起。小红书Android整体上的风格和iOS保持一致,不过我们在15年初开始,对于App内的细节进行Material Design 适配,包括一些按钮风格、点击效果、字体规范、对话框等等,希望为Android用户带来更好的使用体验。

技术选型进化

在技术选型上,这里主要讲一下网络层的框架选型升级和图片加载库的升级。

网络框架的演进

App的最初框架是由钟大侠一人花了10来天完成,包括基本的网络请求框架、App大体的架构以及一些主要的功能。最初时候选择框架的原则就是选择自己最熟悉的,因此我们采用了async-http这套框架作为我们底层的网络请求框架,框架完成了网络的异步请求与回调,能够满足当时的需求。

然而仅仅不到半年之后,我们就决定了使用Volley来替换。替换以后,底层的网络请求代码更加清晰,在Volley返回的结果即直接返回了我们需要的Object,同时将统一的错误处理、公共的参数处理和一些公共的返回使用的参数,全部放在我们自定义的Request当中,这样外部请求所需要传入的参数更少,对于错误的处理更加简单,只需要考虑业务需要的Response,其他全局的返回内容则无需进行干扰。通过Volley的引入,帮助我们在业务的开发上变得更加便捷。引入Volley之初,Volley的底层使用的是HttpClient+HttpURLConnection,后期通过网上的资料发现OkHttp使用NIO更加高效,并且被Android 引入作为系统底层的网络请求,我们也将Volley的底层也替换为OkHttp。

与此同时,小红书的api请求也在不断进行RESTful,我们遇到一个问题就是经常找一个api的定义比较麻烦。大约在15年11月份,我们引入了Retrofit,通过二次改造,使其支持了公共参数的构建,以及对于GsonConvert的改进支持直接返回我们需要的Object,而且对于RESTful风格的良好支持给我们提供了极大的便利。配合RxJava,我们可以方便的进行多个api的同时请求、api返回的多个线程的切换。

图片加载框架的演进

小红书的笔记是以图片加文字为主体的内容,因此会有大量的图片显示需求。和网络框架选型类似,早期选择了比较熟悉的UIL来做图片加载,可以同时支持本地图片和网络图片的加载,在当时可以满足我们的基本需求。

15年初,我们开始使用更加高清的图片,随之加载速度变慢,占用更多的内存,而且这个时候UIL的作者基本很少维护。我们开始调研使用新的图片加载框架。此时Fresco刚刚出来,还不太稳定,当时没敢用。给我们的可选项有Picasso和Glide两个可选项,Picasso比较轻量,但是相比于UIL在性能上没有太好的提高。Glide代码量较大,不过它会在本地保存多份缓存(原始图片和实际显示尺寸的图片),这样加载本地缓存的时候,可以直接显示大小刚好的尺寸,减少解码的时间,因此会比UIL要快很多。

15年下半年,我们需要支持gif的动画显示,而Glide对动画的兼容性又不是特别好,这个时候我们直接切到了Fresco。同时Fresco对webp的良好支持,使得我们在后期切换到webp格式的时候,减少了很多工作量。Fresco在4.4及以下版本使用匿名内存来作为内存缓存,为我们减少OOM做了巨大的贡献。

我们使用的这几个图片加载框架,每个框架的使用都有非常大的区别,这就导致迁移的时候工作量巨大。为了降低迁移成本,我们封装了自己的ImageLoader,在ImageLoader中来实现具体的图片加载,这样保证在迁移的时候,最大程度的降低代码的改动(不过在迁移到Fresco的时候还是改动巨大,因为我们不能直接使用ImageView了o(︶︿︶)o。

推送的升级

推送,我觉得也有必要说一说。最初我们快速选用了百度云推送,在当时看来百度的推送比较稳定,同时接入比较简单。实际使用了一年之后,发现送达率不是特别高,并且数据统计做的不太好,无法比较好的统计推送效果。在调研之后,我们决定迁移到小米推送+友盟推送的模式,针对小米用户开启小米推送,其他用户采用友盟推送,为了平滑过渡,在切换期间同时向未升级的老用户继续使用百度云推送进行推送。

架构升级

由于一直以来在业务开发占用的时间比较多,目前App的整体架构没有做过太大的改变。

在Adapter的使用方面,我们将ListView或RecyclerView的Item放到单独的ItemHander,这样可以在不同的页面可以通过将不同的Item组装到一起,从而满足不同地方的需求。这样可以在ListView或RecyclerView来复用相同的代码,提高代码的可维护性。

前面网络层说到我们的错误处理,这个也是做过比较大的升级。最初时候,网络错误、http请求错误、后台和客户端的错误,都分别在不同的层级进行处理。目前我们在发生错误的时候将错误全部以Exception的方式抛出,最后在上层进行错误的处理。

App中的状态同步,早期使用使用数据库缓存部分数据,或者使用LocalBroadcast进行广播通讯,前者有很多的限制,后者使用起来较为复杂。近期我们改用EventBus进行状态同步,同时这样也使得各个页面之间的耦合也低。

App中占比很大的部分是从网络请求数据,获得数据后进行展示,还是以MVC为主。在一些模块的部分地方,做一些databinding,MVP等的测试。后面有机会会更多大范围的重构。

其他周边进化

我们的开发最初是使用Eclipse进行开发的,但是Eclipse仅仅存在了不到一个月。在我苦口婆心的劝说下,钟大侠和我一起切换到了Android Studio。而这导致我们的项目目录一直都是使用Eclipse时代的目录格式,直到今年年初才切换到Android Studio推荐的目录格式,切换完目录为我们做debug和release差异化提供了极大的便利。

APK最初大约只有5M,历史最高峰达到了23M,在App减肥上我们也做了一些努力,主要是使用tinypng压缩图片,so只保留arm的支持。项目的复杂也使得每次编译都变得很慢,关于这个可以看下我以前的gradle加速http://blog.isming.me/2015/03/18/android-build-speed-up/

现在持续集成还是蛮火的,自然我们也在用。最初的时候,我们每天需要手动打包,打完包之后打开fir的网站,将apk传上去,然后在公司的微信群吼一声,告诉大家我们发包了。经历一段时间后,我们编写了一个Gradle插件帮助我们自动上传到fir,在之后我们搭建了Jenkins自动完成这一系列步骤,并通过邮件告知大家,然后就可以愉快的玩耍了。

Jenkins

未完待续

本文介绍了我们两年来的一些大的变化,通过一篇文章可能很多东西还是说不清楚,暂时就写这么多。目前项目的组织架构还没有特别大的变化,我们目前已经在做一些小范围的测试,后面将对继续不断的进化和演进。

看完评论一下吧

Path和Property Animation配合让线条动起来

2016-06-07 14:03:39

之前做过一个图上标签但是动画样式不太好看,经过查找资料发现了一种全新的思路来实现动画,流畅的让标签的线显示和隐藏,示例如下,就在这里说一说。本文会涉及到Path,Property Animation, PathEffect, PathMeasure。我们开始一一道来。

示例

使用Path绘制曲线

当我们需要画曲线的时候,可能会直接使用drawLine来画,不太复杂的话还比较好实现,如果需要画曲线,或者拐弯的线的时候使用drawLine就比较复杂了。这时候,我们可以借助Path来drawPath。

1
2
3
4
5
6
7
8
9
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE); //一定要设置为画线条
Path path = new Path();
path.moveTo(100, 100);   //定位path的起点
path.lineTo(100, 200);
path.lineTo(200, 150);
path.close();
canvas.drawPath(path, paint);

通过以上的方法代码我们就可以画出三角形了。

测量Path的长度

实现动画的前提是首先得到Path的长度,然后根据长度计算出每个时间节点应该显示的长度。因为系统给我们提供了测量长度的方法,就不需要我们去进行复杂的计算了。直接使用PathMeasure就可以了。

1
2
PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();

只绘制Path的一部分

为了让Path能够逐步显示出来,或者逐步隐藏。我们需要做到能够显示path的一部分,并且改变显示的长度。我们知道可以通过DashPathEffect来显示虚线效果。同时我们可以借助DashPathEffect让我们的实线和虚线的部分的长度分别为我们的Path的长度,然后来改变偏移量,实现只显示path的一部分。

1
2
3
PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength/2);
paint.setPathEffect(effect);
canvas.drawPath(path, paint)

让Path动起来

通过上面说的,我们改变PathEffect的偏移量就可以改变path显示的长度,因此我们可以给我们的View或者对象定义个属性,通过Property Animation来改变这个属性的值,即可实现动画。

PathEffect 属性值变化

1
2
float percentage = 0.0f;
PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength - pathLength*percentage);

动画定义:

1
Animator animatorLine = ObjectAnimator.ofFloat(view, percentage, 0.0f, 1.0f);

其他

就这样就实现了。思路甚至代码都是参考一篇国外的博客。思路很重要,一年前做这个动画的时候百思不得姐,花了好多时间,后面实现的效果还是比较僵硬。这次发现了其他人的思路之后,很容易就解决了。

思路很重要,以及要了解更加全面的知识,不然很多东西都不知道,自己的思路还是会被限制。

最后就是多google,百毒上除了广告,别的东西都挺难找到的。

没有Demo了,可以参考我参考的那个github的库吧。同时作者已经实现svg的动画显示了,原理相同,只是把svg加载为path,使用同样的动画。代码:https://github.com/matthewrkula/AnimatedPathView

看完评论一下吧

Android系统更改状态栏字体颜色

2016-01-09 00:41:39

随着时代的发展,Android的状态栏都不是乌黑一片了,在Android4.4之后我们可以修改状态栏的颜色或者让我们自己的View延伸到状态栏下面。我们可以进行更多的定制化了,然而有的时候我们使用的是淡色的颜色比如白色,由于状态栏上面的文字为白色,这样的话状态栏上面的文字就无法看清了。因此本文提供一些解决方案,可以是MIUI6+,Flyme4+,Android6.0+支持切换状态栏的文字颜色为暗色。

修改MIUI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    public static boolean setMiuiStatusBarDarkMode(Activity activity, boolean darkmode) {
        Class<? extends Window> clazz = activity.getWindow().getClass();
        try {
            int darkModeFlag = 0;
            Class<?> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
            Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
            darkModeFlag = field.getInt(layoutParams);
            Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
            extraFlagField.invoke(activity.getWindow(), darkmode ? darkModeFlag : 0, darkModeFlag);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

上面为小米官方提供的解决方案,主要为MIUI内置了可以修改状态栏的模式,支持Dark和Light两种模式。

修改Flyme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public static boolean setMeizuStatusBarDarkIcon(Activity activity, boolean dark) {
        boolean result = false;
        if (activity != null) {
            try {
                WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
                Field darkFlag = WindowManager.LayoutParams.class
                        .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
                Field meizuFlags = WindowManager.LayoutParams.class
                        .getDeclaredField("meizuFlags");
                darkFlag.setAccessible(true);
                meizuFlags.setAccessible(true);
                int bit = darkFlag.getInt(null);
                int value = meizuFlags.getInt(lp);
                if (dark) {
                    value |= bit;
                } else {
                    value &= ~bit;
                }
                meizuFlags.setInt(lp, value);
                activity.getWindow().setAttributes(lp);
                result = true;
            } catch (Exception e) {
            }
        }
        return result;
    }

同理使用跟miui类似的方式

修改Android6.0+

Android 6.0开始,谷歌官方提供了支持,在style属性中配置android:windowLightStatusBar 即可, 设置为true时,当statusbar的背景颜色为淡色时,statusbar的文字颜色会变成灰色,为false时同理。

1
2
3
4
<style name="statusBarStyle" parent="@android:style/Theme.DeviceDefault.Light">
    <item name="android:statusBarColor">@color/status_bar_color</item>
    <item name="android:windowLightStatusBar">false</item>
</style>

目前为止,android6.0的市场占有率还很少,而MIUI和flyme在国内占有率还算可以,因此,我们可以尽自己所能,适配更多。如果你还有其他的奇淫技巧,也欢迎分享补充。

看完评论一下吧

Android WebView 上传文件支持全解析

2015-12-21 20:02:30

默认情况下情况下,使用Android的WebView是不能够支持上传文件的。而这个,也是在我们的前端工程师告知之后才了解的。因为Android的每个版本WebView的实现有差异,因此需要对不同版本去适配。花了一点时间,参考别人的代码,这个问题已经解决,这里把我踩过的坑分享出来。

主要思路是重写WebChromeClient,然后在WebViewActivity中接收选择到的文件Uri,传给页面去上传就可以了。

创建一个WebViewActivity的内部类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class XHSWebChromeClient extends WebChromeClient {

    // For Android 3.0+
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        CLog.i("UPFILE", "in openFile Uri Callback");
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("*/*");
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
    }

    // For Android 3.0+
    public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
        CLog.i("UPFILE", "in openFile Uri Callback has accept Type" + acceptType);
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        String type = TextUtils.isEmpty(acceptType) ? "*/*" : acceptType;
        i.setType(type);
        startActivityForResult(Intent.createChooser(i, "File Chooser"),
                FILECHOOSER_RESULTCODE);
    }

    // For Android 4.1
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        CLog.i("UPFILE", "in openFile Uri Callback has accept Type" + acceptType + "has capture" + capture);
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        String type = TextUtils.isEmpty(acceptType) ? "*/*" : acceptType;
        i.setType(type);
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
    }


//Android 5.0+
    @Override
    @SuppressLint("NewApi")
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        CLog.i("UPFILE", "file chooser params:" + fileChooserParams.toString());
        mUploadMessage = filePathCallback;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        if (fileChooserParams != null && fileChooserParams.getAcceptTypes() != null
                && fileChooserParams.getAcceptTypes().length > 0) {
            i.setType(fileChooserParams.getAcceptTypes()[0]);
        } else {
            i.setType("*/*");
        }
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
        return true;
    }
}

上面openFileChooser是系统未暴露的接口,因此不需要加Override的注解,同时不同版本有不同的参数,其中的参数,第一个ValueCallback用于我们在选择完文件后,接收文件回调到网页内处理,acceptType为接受的文件mime type。在Android 5.0之后,系统提供了onShowFileChooser来让我们实现选择文件的方法,仍然有ValueCallback,在FileChooserParams参数中,同样包括acceptType。我们可以根据acceptType,来打开系统的或者我们自己创建文件选择器。当然如果需要打开相机拍照,也可以自己去使用打开相机拍照的Intent去打开即可。

处理选择的文件

以上是打开响应的选择文件的界面,我们还需要处理接收到文件之后,传给网页来响应。因为我们前面是使用startActivityForResult来打开的选择页面,我们会在onActivityResult中接收到选择的结果。Show code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == FILECHOOSER_RESULTCODE) {
        if (null == mUploadMessage) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (result == null) {
            mUploadMessage.onReceiveValue(null);
            mUploadMessage = null;
            return;
        }
        CLog.i("UPFILE", "onActivityResult" + result.toString());
        String path =  FileUtils.getPath(this, result);
        if (TextUtils.isEmpty(path)) {
            mUploadMessage.onReceiveValue(null);
            mUploadMessage = null;
            return;
        }
        Uri uri = Uri.fromFile(new File(path));
        CLog.i("UPFILE", "onActivityResult after parser uri:" + uri.toString());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mUploadMessage.onReceiveValue(new Uri[]{uri});
        } else {
            mUploadMessage.onReceiveValue(uri);
        }

        mUploadMessage = null;
    }
}

以上代码主要就是调用ValueCallback的onReceiveValue方法,将结果传回web。

注意,其他要说的,重要

由于不同版本的差别,Android 5.0以下的版本,ValueCallback 的onReceiveValue接收的参数类型是Uri, 5.0及以上版本接收的是Uri数组,在传值的时候需要注意。

选择文件会使用系统提供的组件或者其他支持的app,返回的uri有的直接是文件的url,有的是contentprovider的uri,因此我们需要统一处理一下,转成文件的uri,可参考以下代码(获取文件的路径)。

调用getPath可以将Uri转成真实文件的Path,然后可以自己生成文件的Uri

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class FileUtils {
    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    /**
     * Get a file path from a Uri. This will get the the path for Storage Access
     * Framework Documents, as well as the _data field for the MediaStore and
     * other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @author paulburke
     */
    @SuppressLint("NewApi")
    public static String getPath(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

}

再有,即使获取的结果为null,也要传给web,即直接调用mUploadMessage.onReceiveValue(null),否则网页会阻塞。

最后,在打release包的时候,因为我们会混淆,要特别设置不要混淆WebChromeClient子类里面的openFileChooser方法,由于不是继承的方法,所以默认会被混淆,然后就无法选择文件了。

就这样吧。

看完评论一下吧

Android WebView使用的技巧与一些坑

2015-10-18 17:10:09

随着手机性能的提高,以及iOS和Android两个平台的普及,更多的App都会选择两个平台的App都进行开发,在有些时候,为了更加快速的开发,我们会采用hybird方式开发,这个时候我们需要使用webview并且自己进行一些配置。Android的webview在低版本和高版本采用了不同的webkit版本内核,4.4后直接使用了chrome,因此问题很多,这里分享一些我使用过程的一些技巧和遇到的坑。

webview配置

1
2
3
4
mWebview.getSettings().setJavaScriptEnabled(true); //设置允许运行javascript
// HTML5 API flags
mWebview.getSettings().setAppCacheEnabled(true);  //设置允许缓存
mWebview.getSettings().setDatabaseEnabled(true); //设置允许使用localstore

上面webview.getSettings()会获得WebSettings对象,在这个对象中会保存Webview的一些设置,比如上面所设置的这些,更多的设置请查看WebSettings的api文档。

通常我们还会使用WebViewClient和WebChromeClient这两个组件来辅助WebView。WebViewClient主要帮助处理各种通知请求事件等,比如页面开始加载,加载完成等。WebChromeClient主要辅助WebView处理javascript对话框,网站图标,网站标题,加载进度等等。 实际应该根据实际情况使用这两个组件,重写响应的方法,在其中执行自己的一些操作。

Javascript的使用

开启javascript的方法上面已经提到了。

客户端调用网页中的js代码,或者执行相应的代码。

1
2
3
4
5
6
7
private void evaluateJavascript(String js) {  
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        mWebview.evaluateJavascript(js, null);
    } else {
        mWebview.loadUrl(js);
    }
}

在android4.4开始系统提供了evaluateJavascript方法来执行js方法,并且可以进行回调。但是在低于4.4的版本并没有这个方法,我们需要只要直接通过loadUrl的方式来执行js,此时需要在js代码前加”javascript:”。

另外可以在客户端定义一些javascript给网页中调用。 比如这样:

首先定义一个给js执行的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context */
    WebAppInterface(Context c) {
        mContext = c;
    }

    /** Show a toast from the web page */
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

webView.addJavascriptInterface(new WebAppInterface(this), "Android");

之后用*addJavascriptInterface&设置到webview上,在js中就可以用Android.showToast(“fdf")调用了。

需要注意的是,在我们给js的接口方法需要是public的,使用到了JavascriptInterface的注解,这个注解在Android4.2的时候添加,更新的android如果不加这个注解是不可以使用的。

硬件加速

硬件加速是个大坑,请勿打开。 在android4.4后使用的chrome,系统会自行开启。

其他

以及使用WebView,给忘了给应用申请网络访问的权限。

还有一些知识点没整理到,请参考webview的文档,更多的坑以后踩到再更新。

另外JeremyHe总结的知识也不错,可以参考:http://zlv.me/posts/2015/01/14/08_Android-Webview%E4%BD%BF%E7%94%A8%E5%B0%8F%E7%BB%93/

看完评论一下吧

改变support中AlertDialog的样式

2015-08-31 19:57:15

android最近的support库提供了AlertDialog,可以让我们在低于5.0的系统使用到跟5.0系统一样的Material Design风格的对话框,但是使用了一段时间想到一些办法去改变对话框按钮字体的颜色,都不生效。

最近在网上找到了改变的方法,首先来说一下。

改变AlertDialog的样式

在xml中定义一个主题:

1
2
3
4
5
6
7
8
<style name="MyAlertDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert">
    <!-- Used for the buttons -->
    <item name="colorAccent">#FFC107</item>
    <!-- Used for the title and text -->
    <item name="android:textColorPrimary">#FFFFFF</item>
    <!-- Used for the background -->
    <item name="android:background">#4CAF50</item>
</style>

样式如下图所示:

在创建的对话框的时候,这样创建就可以了。

1
2
3
4
5
6
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.MyAlertDialogStyle);
builder.setTitle("AppCompatDialog");
builder.setMessage("Lorem ipsum dolor...");
builder.setPositiveButton("OK", null);
builder.setNegativeButton("Cancel", null);
builder.show();

这样的方法是每个地方使用的时候,都要在构造函数传我们的这个Dialog的Theme,我们也可以全局的定义对话框的样式。

1
2
3
4
<style name="MyTheme" parent="Base.Theme.AppCompat.Light">
    <item name="alertDialogTheme">@style/MyAlertDialogStyle</item>
    <item name="colorAccent">@color/accent</item>
</style>

在我们的AndroidManifest.xml文件中声明application或者activity的时候设置theme为MyTheme即可,不过需要注意的一点是,我们的Activity需要继承自AppCompatActivity。

其他

从上面改变对话框的样式,可以想到用同样的思路来实现应用的换肤,应用主题之类的功能。

看完评论一下吧

一个上传apk到fir的gradle插件

2015-08-01 23:19:07

声明,这不是广告,没有任何利益瓜葛。

App内测需要把安装把安装包放在一个地方进行托管,方便内测人员下载。国内有蒲公英,fir,等等这些网站可以用。

最近fir上了新版本了,上了新的api,新界面,本以为它们会提供gradle的上传工具,结果没有,而且它们新版本还不好用,原本的下载统计浏览统计都没有了,结果上传很慢,甚至上传不了,我便写了一个gradle的上传工具。

先介绍使用方法吧

使用方法

插件目前只有唯一一个task

uploadFir –上传apk到fir

集成插件本插件,你要按照如下方法使用

编辑build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
buildscript {
  repositories {
    jcenter()
  }

  dependencies {
        classpath 'com.squareup.okhttp:okhttp:2.2.0'
        classpath 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
        classpath 'org.json:json:20090211'
        classpath 'me.isming:firup:0.4.1'
  }
}

apply plugin: 'me.isming.fir'

fir {
    appId = ""   //app的appid,在fir中可以找到
    userToken = ""  //fir用户的token,也在在fir中找到

    apks {
        release {
            // 要上传的apk的路径,类似下面
            sourceFile  file("/project/main/build/outputs/apk/xxx.apk")
            name ""  //app的名称
            version "3.3.0"  //app的版本version
            build "330"   //app的版本号
            changelog ""  //更新日志
            icon file("....../res/drawable-xxhdpi/icon_logo.png")  //app的icon的路径
        }
    }
}

####运行

$ ./gradlew uploadFir

你也可以在本任务的基础上,在你的build脚本中增加以下内容:

1
uploadFir.dependsOn assembleRelease  //后面为你生成apk的任务

这样就可以在执行上传到fir之前首先会生成一个最新的安装包了

本插件基于fir.im官方提供的api文档进行编写,时间匆忙,可能还有一些地方不够完善,还有许多地方可以优化,欢迎star,fork,共同完善。

也可以给我提意见,我来优化。

还有一些代优化的点没有做,后面有空会做,version,build,icon通过程序自动做,而不用手工填写。

项目托管在github上面,生成的jar放在jcenter上面。

github地址:https://github.com/sangmingming/gradle-fir-plugin

原文地址:http://blog.isming.me/2015/08/01/gradle-fir-plugin/,转载请注明出处。

看完评论一下吧

Android应用使用自定义字体的一些探究

2015-07-07 19:04:22

最近团队里面在做程序界面统一的工作,因此希望统一字体,接到一个研究怎么自定义字体的任务。因为我们的开发模式,所以需要研究在界面内的字体自定义,以及webview的显示中的字体自定义。

android系统内置字体

android 系统本身内置了一些字体,可以在程序中使用,并且支持在xml配置textView的时候进行修改字体的样式。支持字段为android:textStyle ,android:typeface, android:fontFamily,系统内置了normal|bold|italic三种style, 内置了normalsans,serif,monospace,几种字体(实测这几种字体仅英文有效),typace和fontFamily功能一样。

使用自定义的字体

以上的方式可以改变字体的样式,还不是真正的自定义。

android系统支持TypeFace,即ttf的字体文件。

我们可以在程序中放入ttf字体文件,在程序中使用Typeface设置字 体。

第一步,在assets目录下新建fonts目录,把ttf字体文件放到这。 第二步,程序中调用:

1
2
3
AssetManager mgr=getAssets();//得到AssetManager
Typeface tf=Typeface.createFromAsset(mgr, "fonts/ttf.ttf");//根据路径得到Typeface
tv.setTypeface(tf);//设置字体

注意ttf文件命名不能使用中文,否则可能无法加载。

对于需要使用比较多的地方,可以写一个TextView的子类来统一处理。

在webview中使用自定义地体

对于本地的网页,在asset目录放字体文件,并在css中添加以下内容,自定义一个字体face,并且在需要的地方使用这个字体face即可。

1
2
3
4
5
6
@font-face {
	font-family: "MyFont";
	src: url('file:///android_asset/fonts/ttf.ttf');
}

h3 { font-family:"MyFont"}

对于在线的网页,则需要把字体文件放到服务器,使用同样的方式定义字体face,应用到每个地方。

为了减少网页或者说服务器端的工作,可以使用本地注入的方式注入font-face的css,并对整个网页进行样式替换。

给webview自定义webViewClient,重写onPageFinish,在其中添加如下内容:

1
2
3
4
5
view.loadUrl("javascript:!function(){" +
        "s=document.createElement('style');s.innerHTML="
        + "\"@font-face{font-family:myhyqh;src:url('**injection**/hyqh.ttf');}*{font-family:myhyqh !important;}\";"
        + "document.getElementsByTagName('head')[0].appendChild(s);" +
        "document.getElementsByTagName('body')[0].style.fontFamily = \"myhyqh\";}()");

由于网页上是没有权限访问本地的asset文件夹的,因此我们需要拦截请求来加载本地的文件,我这里替换了file:///android_assets/**injection**/了,我们还需要重写 shouldInterceptRequest 在请求为我们这个字体文件的时候,加载本地文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    WebResourceResponse response =  super.shouldInterceptRequest(view, url);
    CLog.i("load intercept request:" + url);
    if (url != null && url.contains("**injection**/")) {

        //String assertPath = url.replace("**injection**/", "");
        String assertPath = url.substring(url.indexOf("**injection**/") + "**injection**/".length(), url.length());
        try {

            response = new WebResourceResponse("application/x-font-ttf",
                    "UTF8", getAssets().open(assertPath)
                    );
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return response;
}

问题

使用字体统一界面,但是也遇到了一些问题,如下:

  1. 运行速度变慢(毫秒级,用户觉查不到),由于需要读取自定义的字体文件,以及需要渲染,比使用系统字体要慢。
  2. emoji在5.0以下的系统会有问题。
  3. 在网页,如果采用服务器文件的方法,会消耗用户的流量
  4. 在网页,采用本地注入方式,因为是在onpagefinish后才开始加载字体,因此页面会重新渲染,影响效果。这样还会造成网页可能会出现样式错误。

因为我们的程序中大量使用到emoji,以及考虑到性能的问题,决定还是使用系统自带的字体了。

如果你在这方面有更好的方案,欢迎交流!

看完评论一下吧

图片贴纸旋转缩放功能的实现

2015-05-10 22:55:04

我们的项目包含图片编辑功能,特别是包含图片添加水印贴纸的功能,从最初的简单版可以添加一个图片并且移动位置,到现在添加的图片可以进行移动,以及缩放,旋转,已经是和其他的图片处理可以达到一样的很好的效果了。一直想要整理一下,分享一下实现的改进过程,一直没空,也由于我过于懒,没有动笔。今天正好有时间,分享一下。

原始阶段:直接添加ImageView,并且设置其在父view中的位置

父视图为RelativeLayout,贴纸view就是一个ImageView,通过设置topMargin和leftMargin来设置在父视图中显示的位置,不支持缩放和旋转。功能快速实现,代码比较冗余。再有了新的需求不方便扩展。

新阶段:自定义View,通过matrix变换实现各种功能

主要是定义一个View,在使用的时候放到需要用到的地方,大小设置和目标图片相同大小。通过matrix对平移,旋转,缩放的操作进行映射,最终改变贴纸图片的绘制结果,因此实现目标功能。下面具体分析各个功能。

首先创建的视图在设置完贴纸图片之后,要创建一个浮点型数组,用于保存默认未进行任何变换的时候贴纸图片的关键点,以及一个原始矩形用于保存一个默认绘制区域的矩形,用代码表示就是:

1
2
3
4
float imgWidth = mBitmap.getWidth();
float imgHeight = mBitmap.getHeight();
float[] originPoints = new float[]{0, 0, imgWidth,0, imgWidth, imgHeight, 0, imgHeight, imgWidth/2, imgHeight/2}; //分别为矩形的四个点,与中心点
RectF mOriginRect = new RectF(0, 0, imgWidth, imgHeight);

变换后的点通过Matrix.mapPoints(newPoints, originPoints)进行映射,变换后的矩形通过Matrix.mapRect(newRect, originRect)进行映射,可以通过这些新的点画一些附加元素。至于贴纸图,可以通过获取后的rect进行定位画,也可以直接使用canvas.drawBitmap(bitmap, matrix, paint)方法绘制。

至于如何进行变换操作,如何进行变换,则是在onTouch中处理各种触摸事件,或者在dispatchTouchEvent。

平移

通过判断ACTION_DOWN,ACTION_UP,判断触摸是否在我们的贴纸图片上面,然后计算手指滑动的距离,可以获取到x轴和y轴的平移距离,调用mMatrix.postTranslate(x,y),然后重新映射绘图即可。

旋转

以贴纸图片的一个边缘点为旋转触摸点,以贴纸图片的中心(非贴纸view的中心),计算旋转的角度,调用mMatrix.postRotate(rotation, px, py), px,py为贴纸图片的中心点(为上面映射后的点,而不是原始点)。

缩放

同样通过触摸位置计算两次滑动过程中的缩放比例,来通过Matrix.postScale(scale, scale, px, py)进行缩放。

其他

开始的时候没有想到使用Matrix,进行了很多的尝试,没有很好的结果。最后使用了Matrix之后,则简单很多,只是在计算缩放和旋转的时候,因为数学没有学好,花了很久才把数学问题搞定。

这里分享我自己的一个完整的贴纸View,开箱即用,https://github.com/sangmingming/StickerView 。如果在这方面,你又更好的实现方式,也欢迎留言,与我进行交流。

看完评论一下吧

打破Android应用65K方法数魔咒

2015-05-01 00:33:08

近日,我们的应用,在编译的时候不幸的遇到这个错误

Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

这才让我意识到原来我们的程序中,方法数已经超过了65536。在之前,已经知道了android系统的java虚拟机dalvik在执行java程序时,使用原生类型short来索引dex文件中的方法,因此方法数就呗限制在65536了。之前我一直以为,这个数量已经很大了,不会达到上限,结果今天就达到了。

不过这个东西呢,我们也是很容易的进行解决的,因为,就在去年不久前,google官方提供了多dex的支持库,因此,我们可以很简单的解决这个问题。

开发工具升级

将android sdks build tools 和android support library要升级到最新的,这个使用android sdks manager很容易就完成了。

配置build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
android { 
compileSdkVersion 21 
buildToolsVersion "21.1.0" 
defaultConfig {     
...     
minSdkVersion 14     
targetSdkVersion 21     
...     
// Enabling multidex support.     
multiDexEnabled true 
} 
... 
} 
dependencies{ 
compile 'com.android.support:multidex:1.0.0 //dependencies
}

让应用支持多dex

androidManifest.xml中application中声明android.support.multidex.MultiDexApplication;

或自己定义一个Application类,继承自MultiDexApplication;

或者自己定义的Application类,在attachBaseContext()方法中,添加MultiDex.install(this);

其他

通过上面的方法即可轻松完成多dex,不过在低版本的android系统(低于android4.0)可能会有bug出现,还要多进行测试。

究其原因,其实我们的app,自己写的代码现在其实不是很多,代码中使用了大量的第三方sdk,以及其他的一些功能集成。

下面,就要想办法,减少第三方的功能库了。这里跟大家分享一下解决方案。

参考资料: http://developer.android.com/tools/building/multidex.html

看完评论一下吧

加速Android Studio/Gradle构建

2015-03-18 22:56:10

已经使用Android Studio进行开发超过一年,随着项目的增大,依赖库的增多,构建速度越来越慢,现在最慢要6分钟才能build一个release的安装包,在网上查找资料,发现可以通过一些配置可以加快速度,这里跟大家分享一下。

开启gradle单独的守护进程

在下面的目录下面创建gradle.properties文件:

并在文件中增加:

1
org.gradle.daemon=true

同时修改项目下的gradle.properties文件也可以优化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Settings specified in this file will override any Gradle settings
# configured through the IDE.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# The Gradle daemon aims to improve the startup and execution time of Gradle.
# When set to true the Gradle daemon is to run the build.
# TODO: disable daemon on CI, since builds should be clean and reliable on servers
org.gradle.daemon=true

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true

# Enables new incubating mode that makes Gradle selective when configuring projects. 
# Only relevant projects are configured which results in faster builds for large multi-projects.
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:configuration_on_demand
org.gradle.configureondemand=true

同时上面的这些参数也可以配置到前面的用户目录下的gradle.properties文件里,那样就不是针对一个项目生效,而是针对所有项目生效。

上面的配置文件主要就是做, 增大gradle运行的java虚拟机的大小,让gradle在编译的时候使用独立进程,让gradle可以平行的运行。

修改android studio配置

在android studio的配置中,开启offline模式,以及修改配置。实际上的配置和上面的一大段一样,主要是在这个地方配置的只会在ide构建的时候生效,命令行构建不会生效。

开启offline

设置运行模式和VM配置

命令行构建

基于上面的配置,命令行构建时在命令后面加上这个参数即可 --daemon --parallel --offline

引入依赖库时使用aar

使用网上第三方的依赖库时尽量使用aar,可以在mavenhttp://gradleplease.appspot.com/或者githuhttps://github.com/Goddchen/mvn-repo搜索。

自己的库模块也可以打包成aar,关于这个可以参考stormzhang的文章http://www.stormzhang.com/android/2015/03/01/android-reference-local-aar/

后记

经过这样一番折腾,我原来需要4,5分钟才能构建完成的项目,现在只需要30秒左右就可以构建完成了。当然了,如果你这样还是不可以的话,那么,你应该换电脑了,ssd的硬盘,更大的内存,必须的上了。

看完评论一下吧

java注解

2015-03-06 00:19:19

从java 5.0开始,为我们提供注解功能,通过注解可以限制代码的重载,过时,以及实现一些其他功能,这里,就来分析一下java的注解。

java 元注解

首先来看java元注解,分别是:

@Target

@Retention

@Documented

@Inherited

这些注解和他们所修饰的类在java.lang.annotation包中,代码都很简单,可以去查看一下。

@Target 描述注解的使用范围,取值:

ElementType.CONSTRUCTOR:描述构造器 ElementType.FIELD:描述成员变量 ElementType.VARIABLE: 描述局部变量 ElementType.METHOD: 描述方法 ElementType.PACKAGE: 描述包 ElementType.PARAMETER:描述方法的参数 ElementType.Type: 描述类,接口(包括注解类型)或enum声明.

@Retention 注解的声明周期,即在什么级别保留,取值:

RetentionPoicy.SOURCE :在源文件中有效(在.java文件中有效)
RetentionPoicy.CLASS: 在class文件中有效 RetentionPoicy.RUNTIME:在运行时有效

@Documented 用于描述其他类型的annotation应该被作为被标注的程序成员的公共API,可以被javdoc的工具文档化,无成员。

@Inherited 用于标注某个标注是被继承的,即父类中使用了一个Annotation,则子类继承父类的这个annotation,annotation需要标记为RUNTIME的才可以。

java内置注解

以上是元标记,再看java内置的标准注解,@Override,@Deprecated, @SuppressWarnings

@Override

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

从前面的元注解介绍可以看到,Override用于标注方法,有效期是在源码期间。用于标注方法重写。

@Deprecated

1
2
3
4
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
}

标注 过时,或者不建议使用,也是会保留到运行时,添加了Documented元标签,这样在生成文档时候,就可以生成过时的标记。

@SuppressWarnings

1
2
3
4
5
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

忽略错误报告,有效时是源码级。

自定义注解

我们再来看看如何自定义注解。自定义的注解就和java内置的注解类似,也需要用到元注解,通过远注解设置那些地方可以使用,设置作用域。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
     int value() default;
}


@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface MyNewAnnotation{
     String author();
     int version() default 1;
}

public class MyClass {
     @MyAnnotation(12)
     public boolean isOK() {
         return true;
     }

     @MyNewAnnotation(author=sam, version=2)
     public int getAge() {
         return 19;
     }
}

上面前面的代码是定义注解,后面是使用。可以看到使用@interface来定义注解。

注解配置参数名为注解类的方法名,并且方法没有方法体,没有参数没有修饰符,不可以抛异常。返回值只能是基本类型,String,Class,annotation,enumeration,或者他们的一维数组。只有一个默认属性,可以直接用value()函数,没有属性,则这个注解是标记注解。可以加default表示默认值。

Android内置注解

作为android程序员,我们还是了解一下android中自带的注解,以及用法含义。

@SuppressLint: 指示lint检查时忽略注解元素的警告信息。
@TargetApi:指示lint把当前这个注解元素的target api为指定值,而不是项目设置的target api。
@NonNull:表示一个成员变量,或者参数,或者方法返回值永远不能为NULL。
@Nullable:标识一个成员变量,或者参数,方法返回值,可以为NULL。

android.support.annotation包中还有更多的注解可以使用。

另外,http://codekk.com/open-source-project-analysis/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8BJava%20%E6%B3%A8%E8%A7%A3%20Annotation对于注解的分析很好,推荐一下。

看完评论一下吧

android动画-View Animation

2015-02-01 11:10:50

视图动画(View Animation),又称补间动画(Tween Animation),即给出两个关键帧,通过一些算法将给定属性值在给定的时间内在两个关键帧间渐变。本文首先讲解各种基本动画的使用,其实介绍View动画的工作过程。

概述

视图动画只能作用于View对象,是对View的变换,默认支持的类型有:

可以使用AnimationSet让多个动画集合在一起运行,使用插值器(Interpolator)设置动画的速度。

上面说到的几种动画,以及AnimationSet都是Animation的之类,因此Animation中有的属性,以及xml的配置属性,他们都有,因此,单独说每个动画的时候只说其特有的方法和属性。对于使用xml配置时需要放到res下面的animation文件夹下。

AlphaAnimation 透明度动画

就是改变视图的透明度,可以实现淡入淡出等动画。这个动画比较简单只需要设置开始透明度和结束透明度即可。

1
Animation animation = new AlphaAnimation(0.1f, 1.0f); //fromAlpha 0.1f   toAlpha 1.0f

1
<alpha android:fromAlpha = "0.1f" android:toAlpha="1.0f" />

ScaleAnimation 缩放

缩放动画,支持设置开始x缩放(宽度缩放倍数),开始y缩放, 结束x缩放,结束y缩放,以及缩放基点x坐标,缩放基点y坐标。

x缩放和y缩放都是相对于原始的宽度和高度的,1.0表示不缩放。

坐标基点,同时有参数可以设置坐标基点类型,分别是:

默认基点是视图的0点,默认坐标基点类型是ABSOLUTE。

有如下几种构造函数

1
2
3
4
5
6
7
ScaleAnimation(Context context, AttributeSet attrs)
ScaleAnimation(float fromX, float toX, float fromY, float toY)
new ScaleAnimation(1.0f, 1.5f, 1.0f, 1.5f);
ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY)
new ScaleAnimation(1.0f, 1.5f, 1.0f, 1.5f, 10, 10);
ScaleAnimation(float fromX, float toX, float fromY, float toY, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)
new ScaleAnimation(1.0f, 1.5f, 1.0f, 1.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); //以中心点为基点

XML配置:

1
2
3
4
5
6
7
<scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float" />

TranslateAnimation 位移

平移支持x轴平移起点和y轴平移起点,以及设置结束点。同时每个点都可以设置type,type和上面缩放动画的基点类型一样,默认类型是ABSOLUTE.

有以下几个构造函数:

1
2
3
TranslateAnimation(Context context, AttributeSet attrs)
TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
TranslateAnimation(int fromXType, float fromXValue, int toXType, float toXValue, int fromYType, float fromYValue, int toYType, float toYValue)

XML配置:

1
2
3
4
5
<translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float" />

RoatationAnimation 旋转

旋转支持设置旋转开始角度,和旋转结束角度,以及旋转基点,和旋转基点类型。类型同上面一样,默认旋转基点是(0,0),默认类型同上面一样,也不多说了。

1
2
3
4
RotateAnimation(Context context, AttributeSet attrs)
RotateAnimation(float fromDegrees, float toDegrees)
RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY)
RotateAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)

XML配置:

1
2
3
4
5
<rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float" />

AnimationSet 动画集合

动画集合就是可以让多个动画一起运行,或者依次运行。

通过addAnimation(Animation a)向集合中添加动画,使用子动画的setStartOffset(long offset)设置延时,从而实现子动画之间的间隔。可以设置是否共享时间插值器。

xml配置:

1
2
3
4
5
<set>
<!--这里写子动画-->
<rotation ..../>
<alpha ...../>
</set>

属性动画(Property Animation)

Animation

单独把Animation拿出来说,是因为前面几个都是Animation,他们有一些属性都是从父类继承的。包括时常,插值器,是否重复,监听器等。

setFillBefore(boolean)和setFillAfter(boolean)分别是动画开始前和动画结束后是否保持动画状态,默认前者为ture,后者为false;

xml中可以配置的属性(这些在前面几个动画中省略了,也是可以使用的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
android:detachWallpaper
android:duration
android:fillAfter
android:fillBefore
android:fillEnabled
android:interpolator
android:repeatCount
android:repeatMode   INFINTE(无限期),RESTART(重新开始,默认值)
android:startOffset
android:zAdjustment   ZORDER_BOTTOM,ZORDER_NORMAL, ZORDER_TOP

启动动画:

1
2
3
4
view.startAnimation(animation);
//或者这样
view.setAnimation(animation);
animation.start();

Interpolator 插值器

通过设置插值器可以改变动画的速度,以及最终效果。 android sdk提供了几种默认插值器,而且这些插值器在新的protery animation上仍然可以使用,这个后面再说。

当然,我们也可以自定义Interpolator,一般开始值为0,结束值为1.0,然后根据算法来改变值。

动画原理解析

动画就是根据间隔时间,不停的去刷新界面,把时间分片,在那个时间片,通过传入插值器的值到Animation.applyTransformation(),来计算当前的值(比如旋转角度值,透明度等).

因此,我们也可以继承Animation,从写applyTransformation()来实现我们的其他的动画。

其他

使用view动画时,如果需要用到类似基点类型和基点设置的,一定要注意设置对点,不然效果恨不如意。

另外,view动画,若动画前view在a点,动画过程以及动画后,view变化了位置,则点击点仍然在原位置,这是个大问题,特别需要注意。

在android apidemo中,有动画的使用,以及自定义动画,各种插值器效果,各位可以查看,我已经将其放到github上面了,地址:https://github.com/sangmingming/Android-ApiDemos

看完评论一下吧

android动画-Frame Animation

2015-01-28 20:13:47

动画可以在视觉上增加程序的流畅度,我之前对于动画这一块,是会用,但是不全面,这里写下博客,全面梳理一下Android动画方面的知识。当然,关于动画这块,也有很多前人写了很多内容,大家可以去参考。

3.0以前,android支持两种动画模式,Tween Animation,Frame Animation,在android3.0中又引入了一个新的动画系统:Property Animation,这三种动画模式在SDK中被称为Property Animation,View Animation,Drawable Animation。 可通过NineOldAndroids项目在3.0之前的系统中使用Property Animation。另外呢,还有activity之间的过渡动画,android5.0增加的矢量动画,过渡效果等。

本文首先来说Frame Animation.

帧动画,在android中又称Drawable Animation,就是通过一系列的Drawable依次显示来达到模拟动画的效果。 android中提供了AnimationDrawable类来实现帧动画,我们可以使用AnimationDrawable作为View的背景。我们通常可以使用xml来配置动画。

在项目的res/drawable/目录下面创建一个xml文件。

文件中以<animation-list>作为根节点, 每一张图片作为一个<item>

如:

1
2
3
4
5
6
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/rocket_thrust1" android:duration="200" />
    <item android:drawable="@drawable/rocket_thrust2" android:duration="200" />
    <item android:drawable="@drawable/rocket_thrust3" android:duration="200" />
</animation-list>

上面代码中,onshot若为true,则动画只播放一次,否则动画会循环播放。item中的duration用于设置当前帧的停留时间。

在代码中获取并,启动动画。

如上面的xml文件为rocket_thrust.xml,则在代码中使用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
AnimationDrawable rocketAnimation;

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
  rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
  rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
}

public boolean onTouchEvent(MotionEvent event) {
  if (event.getAction() == MotionEvent.ACTION_DOWN) {
    rocketAnimation.start();
    return true;
  }
  return super.onTouchEvent(event);
}

上面代码具体就是首先从资源中获取到我们的动画,然后设置为view的背景,之后启动动画。

需要注意的是,动画的启动需要在view和window建立连接后才可以绘制,比如上面代码是在用户触摸后启动。如果我们需要打开界面就启动动画的话,则可以在Activity的onWindowFocusChanged()方法中启动。

上面介绍的是在xml中定义动画,当然也可以在java代码中定义动画。如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
AnimationDrawable rocketAnimation;

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
 
  rocketAnimation = new AnimationDrawable();
  rocketAnimation.addFrame(getResources().getDrawable(R.drawable.rocket_thrust1, 200);
  rocketAnimation.addFrame(getResources().getDrawable(R.drawable.rocket_thrust2, 200);
  rocketAnimation.addFrame(getResources().getDrawable(R.drawable.rocket_thrust3, 200);
rocketImage.setBackground(rocketAnimation);  
}

public boolean onTouchEvent(MotionEvent event) {
  if (event.getAction() == MotionEvent.ACTION_DOWN) {
    rocketAnimation.start();
    return true;
  }
  return super.onTouchEvent(event);
}

更多使用方法,可以去查看AnimationDrawable的api手册.

另外,看了一下相关代码,AnimationDrawable代码不长,其父类是DrawableContainer,用于保存Drawable list,另外有一个数组保存每一帧的停顿时间。每隔一定时间,替换Drawable,重新刷新,实现动画。

看完评论一下吧

android反编译-如何防止反编译

2015-01-18 01:35:53

前言

前面介绍了怎样去反编译别人的代码。哈哈,这里居然又写进行防止反编译。但是,还是先来写写吧。

使用ProGuard

proguard android的sdk中就有提供,使用它可以对代码进行混淆和精简,处理后的代码,虽然仍然可以反编译,但是阅读起来相当困难,降低代码的可读性。操作简单,推荐使用。

proguard使用方法和配置,可以看我之前的博客: https://isming.me/2014-05-31-use-proguard/

另外网上有别人共享的proguard配置模板,也可以参考: https://github.com/krschultz/android-proguard-snippets

如果大家有去proguard的官网,ProGuard的公司提供的DexGuard可以给android程序提供更多的优化和保护,不过这个软件收费的,有需要的也可以去了解以下(我不是广告,(^_^))。

代码转移到native

代码放在native层的话,使用我前面的方法就没办法去反编译了,这时就需要借助反编译c的方法了,这个我没有研究过了。

因此写在native层也是很安全的,但是因为native更难写,只建议偏重于专利,或者机密数据,等一些功能和逻辑写在native层。更加安全,也更快速。

使用第三方加密工具

国内现在也出现了很多apk加固工具,比如爱加密,梆梆加密等等。这些没有去使用过,但是看过网上的介绍,以及他们的自己的介绍,大致了解到,是在我们的apk之外加壳,对我们的dex文件进行加密来做的。

使用这些工具可以来帮助提高软件的安全性,但是使用之前也要确保服务的可靠性,服务商的信誉。

个人之见

以上只是本人想到的几种,比较可行的方案。同时肯定还有其他的方式,比如采用签名验证,插件开发等等机制。

在我看来,软件的一定程度的混淆是有必要的,毕竟这个一个公司的财产(很多公司靠一个app营收),不过一些不是很特有的东西也是可以开源的。毕竟,现在网上的开源项目很多,我们也从中使用,借鉴了很多,也要回馈开源社区才行

文章系本人拙见,如果这方面你有什么好的方法,或者有什么好的建议,也可以评论交流。

看完评论一下吧

android反编译-smali语法

2015-01-14 23:52:44

前言

前面我们有说过android反编译的工具,如何进行反编译。反编译后可以得到jar或者得到smali文件。Android采用的是java语言进行开发,但是Android系统有自己的虚拟机Dalvik,代码编译最终不是采用的java的class,而是使用的smali。我们反编译得到的代码,jar的话可能很多地方无法正确的解释出来,如果我们反编译的是smali则可以正确的理解程序的意思。因此,我们有必要熟悉smali语法。

类型的表示

java里面包含两种类型,原始类型和引用类型(包括对象),同时映射到smali也是有这两大类型。

原始类型

V void (只能用于返回值类型) Z boolean B byte S short C char I int J long F float D Double

对象类型

Lpackage/name/ObjectName; 相当于java中的package.name.ObjectName

L 表示这是一个对象类型 package/name 该对象所在的包 ObjectName 对象名称 ; 标识对象名称的结束

数组的表示

[I 表示一个int型的一维数组,相当于int[]; 增加一个维度增加一个 [,如*[[I表示int[][]*

数组每一个维度最多 255个;

对象数组表示也是类似,如String数组的表示是 [Ljava/lang/String

寄存器与变量

java中变量都是存放在内存中的,android为了提高性能,变量都是存放在寄存器中的,寄存器为32位,可以支持任何类型,其中long和double是64为的,需要使用两个寄存器保存。

寄存器采用v和p来命名 v表示本地寄存器,p表示参数寄存器,关系如下

如果一个方法有两个本地变量,有三个参数

v0 第一个本地寄存器 v1 第二个本地寄存器 v2 p0 (this) v3 p1 第一个参数 v4 p2 第二个参数 v5 p3 第三个参数

当然,如果是静态方法的话就只有5个寄存器了,不需要存this了。

.registers 使用这个指令指定方法中寄存器的总数 .locals 使用这个指定表明方法中非参寄存器的总数,放在方法的第一行。

方法和字段的表示

方法签名

methodName(III)Lpackage/name/ObjectName;

如果做过ndk开发的对于这样的签名应该很熟悉的,就是这样来标识一个方法的。 上面methodName标识方法名,III表示三个整形参数,Lpackage/name/ObjectName;表示返回值的类型。

方法的表示

Lpackage/name/ObjectName;——>methodName(III)Z 即 package.name.ObjectName中的 function boolean methondName(int a, int b, int c) 类似这样子

字段的表示

Lpackage/name/ObjectName;——>FieldName:Ljava/lang/String;

即表示: 包名,字段名和各字段类型

方法的定义

比如我下面的一个方法

1
2
3
private static int sum(int a, int b) {
        return a+b;
}

使用编译后是这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

.method private static sum(II)I
    .locals 4   #表示需要申请4个本地寄存器
    .parameter
    .parameter #这里表示有两个参数

    .prologue
    .line 27 
    move v0, p0

    .local v0, a:I
    move v1, p1

    .local v1, b:I
    move v2, v0

    move v3, v1

    add-int/2addr v2, v3

    move v0, v2

    .end local v0           #a:I
    return v0
.end method

从上面可以看到函数声明使用*.method开始 .end method结束,java中的关键词private,static 等都可以使用,同时使用签名来表示唯一的方法,这里是sum(II)I*。

声明成员

.field private name:Lpackage/name/ObjectName; 比如:private TextView mTextView;表示就是 .field private mTextView:Landroid/widget/TextView; private int mCount; .field private mCount:I

指令执行

smali字节码是类似于汇编的,如果你有汇编基础,理解起来是非常容易的。

比如: move v0, v3 #把v3寄存器的值移动到寄存器v0上.

const v0, 0x1 #把值0x1赋值到寄存器v0上。

invoke-static {v4, v5}, Lme/isming/myapplication/MainActivity;->sum(II)I #执行方法sum(),v4,v5的值分别作为sum的参数。

其他

通过前面我们可以看到,smali就是类似汇编,其中很多命令,我们可以去查它的手册来一一对应。学习时,我们可以自己写一个比较简单的java文件,然后转成smali文件来对照学习。

下面,我贴一个我写的一个比较简单的java文件以及其对应的smali,其中包含if判断和for循环。

java文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package me.isming.myapplication;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.text);

        mTextView.setText("a+b=" + sum(1,2) + "a>b?" + max(1,2) + "5 accumulate:" + accumulate(5));

    }

    private static int sum(int a, int b) {
        return a+b;
    }

    private boolean max(int a, int b) {
        if (a > b) {
            return true;
        } else {
            return false;
        }
    }
    
    private int accumulate(int a) {
        if (a <= 0) {
            return 0;
        }
        int sum = 0;
        for(int i = 0; i <= a; i++) {
            sum += a;
        }
        return sum;
    }
}

对应的smali:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
.class public Lme/isming/myapplication/MainActivity;
.super Landroid/support/v7/app/ActionBarActivity;
.source "MainActivity.java"


# instance fields
.field private mTextView:Landroid/widget/TextView;


# direct methods
.method public constructor <init>()V
    .locals 2

    .prologue
    .line 10
    move-object v0, p0

    .local v0, this:Lme/isming/myapplication/MainActivity;
    move-object v1, v0

    invoke-direct {v1}, Landroid/support/v7/app/ActionBarActivity;-><init>()V

    return-void
.end method

.method private accumulate(I)I
    .locals 6
    .parameter

    .prologue
    .line 39
    move-object v0, p0

    .local v0, this:Lme/isming/myapplication/MainActivity;
    move v1, p1

    .local v1, a:I
    move v4, v1

    if-gtz v4, :cond_0

    .line 40
    const/4 v4, 0x0

    move v0, v4

    .line 46
    .end local v0           #this:Lme/isming/myapplication/MainActivity;
    :goto_0
    return v0

    .line 42
    .restart local v0       #this:Lme/isming/myapplication/MainActivity;
    :cond_0
    const/4 v4, 0x0

    move v2, v4

    .line 43
    .local v2, sum:I
    const/4 v4, 0x0

    move v3, v4

    .local v3, i:I
    :goto_1
    move v4, v3

    move v5, v1

    if-gt v4, v5, :cond_1

    .line 44
    move v4, v2

    move v5, v1

    add-int/2addr v4, v5

    move v2, v4

    .line 43
    add-int/lit8 v3, v3, 0x1

    goto :goto_1

    .line 46
    :cond_1
    move v4, v2

    move v0, v4

    goto :goto_0
.end method

.method private max(II)Z
    .locals 5
    .parameter
    .parameter

    .prologue
    .line 31
    move-object v0, p0

    .local v0, this:Lme/isming/myapplication/MainActivity;
    move v1, p1

    .local v1, a:I
    move v2, p2

    .local v2, b:I
    move v3, v1

    move v4, v2

    if-le v3, v4, :cond_0

    .line 32
    const/4 v3, 0x1

    move v0, v3

    .line 34
    .end local v0           #this:Lme/isming/myapplication/MainActivity;
    :goto_0
    return v0

    .restart local v0       #this:Lme/isming/myapplication/MainActivity;
    :cond_0
    const/4 v3, 0x0

    move v0, v3

    goto :goto_0
.end method

.method private static sum(II)I
    .locals 4
    .parameter
    .parameter

    .prologue
    .line 27
    move v0, p0

    .local v0, a:I
    move v1, p1

    .local v1, b:I
    move v2, v0

    move v3, v1

    add-int/2addr v2, v3

    move v0, v2

    .end local v0           #a:I
    return v0
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 8
    .parameter

    .prologue
    .line 16
    move-object v0, p0

    .local v0, this:Lme/isming/myapplication/MainActivity;
    move-object v1, p1

    .local v1, savedInstanceState:Landroid/os/Bundle;
    move-object v2, v0

    move-object v3, v1

    invoke-super {v2, v3}, Landroid/support/v7/app/ActionBarActivity;->onCreate(Landroid/os/Bundle;)V

    .line 17
    move-object v2, v0

    const v3, 0x7f030017

    invoke-virtual {v2, v3}, Lme/isming/myapplication/MainActivity;->setContentView(I)V

    .line 19
    move-object v2, v0

    move-object v3, v0

    const v4, 0x7f08003f

    invoke-virtual {v3, v4}, Lme/isming/myapplication/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v3

    check-cast v3, Landroid/widget/TextView;

    iput-object v3, v2, Lme/isming/myapplication/MainActivity;->mTextView:Landroid/widget/TextView;

    .line 21
    move-object v2, v0

    iget-object v2, v2, Lme/isming/myapplication/MainActivity;->mTextView:Landroid/widget/TextView;

    new-instance v3, Ljava/lang/StringBuilder;

    move-object v7, v3

    move-object v3, v7

    move-object v4, v7

    invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V

    const-string v4, "a+b="

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v3

    const/4 v4, 0x1

    const/4 v5, 0x2

    invoke-static {v4, v5}, Lme/isming/myapplication/MainActivity;->sum(II)I

    move-result v4

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v3

    const-string v4, "a>b?"

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v3

    move-object v4, v0

    const/4 v5, 0x1

    const/4 v6, 0x2

    invoke-direct {v4, v5, v6}, Lme/isming/myapplication/MainActivity;->max(II)Z

    move-result v4

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;

    move-result-object v3

    const-string v4, "5 accumulate:"

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v3

    move-object v4, v0

    const/4 v5, 0x5

    invoke-direct {v4, v5}, Lme/isming/myapplication/MainActivity;->accumulate(I)I

    move-result v4

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v3

    invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v3

    invoke-virtual {v2, v3}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 23
    return-void
.end method

参考资料

最后附上一些参考资料:

http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

https://code.google.com/p/smali/w/list

http://www.miui.com/thread-409543-1-1.html

看完评论一下吧

android反编译-反编译工具和方法

2015-01-11 22:19:17

前言

开发过程中有些时候会遇到一些功能,自己不知道该怎么做,然而别的软件里面已经有了,这个时候可以采用反编译的方式,解开其他的程序,来了解一些它的做法,同时啊,还可以借鉴别人的软件结构,资源文件,等等,哈哈。那我就来讲解一些关于反编译相关的知识,主要分三篇,第一篇介绍反编译的工具和方法,第二篇,介绍smali的语法,第三篇介绍如何防止反编译,主要通过这几篇文章,了解如何去做反编译和代码加固。

工具

apktools-目前最强大的反编译工具

轻松反编译apk,解析出资源文件,xml文件,生成smali文件,还可以把修改后的文件你想生成apk。

支持windows,linux,mac。

下载地址:https://code.google.com/p/android-apktool/downloads/list 请自备梯子

dex2jar

将apk中的dex文件转换成为jar文件,很多人不会看smali文件,还是看java类文件比较舒服,这个时候可以借助这个工具来转成java,也是支持windows,linux,mac。

下载地址:http://code.google.com/p/dex2jar/downloads/list

jd-gui

查看jar文件,基本可以看到java class文件了,也是支持mac,windows,linux。

下载地址:http://jd.benow.ca/

apktool的命令行综合工具推荐 apktool plus

其实是别人写的一个工具,集合了apktool的功能,另外还支持给apk签名。最新版本是v9update6,只支持windows系统。

下载地址:http://dl.dbank.com/c0jndlkbu4#

进行反编译

使用apktools

在apktools目录下执行以下命令

./apktool d pathtoapk outdir #mac linux apktool.bat d pathtoapk outdir #window

这样就可以反编译成功了,可以查看其中的资源文件,smali文件,当然有的app进行了特殊处理,不是全部可以反编译的。

同时apktool还可以对反编译后的文件逆向成apk文件,格式如下。

./apktool b apppath outpath

逆向后的文件要是无签名的需要先签名才可以安装。

使用dex2jar

apk文件本身其实就是一个zip压缩包,先讲apk改成一个*.zip*文件解压后得到一个classes.dex。到dex2jar的目录,执行以下命令.

./d2j-dex2jar.sh pathtoclasses.dex #mac linux d2j-dex2jar.bat pathtoclasses.dex #wind

之后会生成一个jar文件,用jd-gui打开就可以看到其中的java代码了。

其他

其实我们使用的反编译也就这些足够了,通常很多时候无法获取很多的代码,毕竟人家也有措施应对的。

看完评论一下吧

2014年总结

2015-01-05 22:24:37

14年总结

又一年开始,网上也流散着许多的年终总结,往年我也会写写,都是流水的回忆。前两日,与小波电话聊天,突然想到问他一四年用三个词总结是什么?得到的是失恋,毕业,工作。让我说说2014的三个词,竟然一时无从说起。这两日一直思考这个问题,毕竟这一年过的比较平淡。用三个关键词形容2014,就是"踏实", “责任"和"收获"吧。

2014年对于我来说,是很平淡的一年,没有什么大的波折,一些都还比较顺。这一年很踏实,这一年读了很多书(可能是我目前为止读书最多的一年),第一年开始认真用心的写博客,同时从广州转战上海,心里上也很踏实。

这一年奶奶去世了,家里还有几个老人,同时父母身体也不好,几次回家,都能感受到父母的担子。妹妹也来到上海开始新的学业,在这边也好有个照应。公司的项目,已经开始了基本单兵作战,要保证软件的质量,项目的工期。男子汉,要扛起责任。

收获,怎么说呢,收获很多,包括个人成长,爱情,友情等等。读书写博,技术上有很多的成长。分享自己的知识以及代码,被别人阅读以及star,收获网上的朋友,同时也鼓励自己继续坚持下去。转战上海,结束异地,同时也离家更近了。能够更多的时间在一起。

2014的最后应该还有一个词,“感谢”,感谢离开广州市,同事的教诲,冯佳的陪酒;感谢现在上级的知遇,同事的协助,小红书让我迈出了很多的第一次;感谢初到上海朋友的帮助,特别是袁义;感谢女友的包容,妹妹的谅解;感谢你的浏览😄😄。

展望15

其实上面的总结,在元旦那天已经发到QQ空间。不过,内容主要是对于14年的一些感想。仍然需要针对15年做一个规划,计划给自己一个执行的参照。主要是让自己能够在技术和知识的素养上有所提升,身体健康等能够提高。

多读书

相比之下,14年已经比以前任何一年读的书更多了,但是不够,需要读更多。暂时以下图书需要重读或者精读。

重构改善既有代码的设计
java虚拟机设计规范
操作系统-精髓与设计原理
深入理解Android
设计模式
更多图书待补充

知识深入

对于Android和Java要更深入,学习java虚拟机,操作系统原理,android系统底层虚拟机等。

知识扩展

学习Python,JavaScript,这两门语言其实已经会了一点,但是要学习,保证工作能够顺利使用,毕竟现在客户端开发都需要服务端的支持,同时客户端Hybird开发h5和Javascript很重要。同时后端的一些技术,比如mongodb数据库,nginx服务器等。

#### 写博客

坚持写博客。 2015年主要开发写,android各种组件的详细使用,帮助自己整理梳理知识。写android和设计模式,主要深入熟悉设计模式,同时写自己在工作中遇到的问题,欢迎提意见。

身体锻炼

锻炼身体,今年刚到上海的时候,坚持了没走两次,天冷后就不跑了,身体已经胖了不少。15年要多跑步,锻炼身体了。

与人交流

多参加一些技术技术交流,多与朋友进行联系。

看完评论一下吧

一个程序员的Ubuntu安装的那些软件

2014-06-22 12:57:52

鄙人程序猿一枚,Android开发,常年使用Ubuntu(主要是买不起Mac,O(∩_∩)O哈哈~)。分享一下自己使用的那些软件,如果你有什么好的软件。欢迎与我交流。

输入法:开始的时候是用的fcitx,后来搜狗出了linux版本,选择之。下载链接

办公软件:WPS Linux,真心很好用,比libreOffice好用不止一点.下载链接 注意:下载alpah版本.

浏览器:chrome 和 FireFox,不解释,一个浏览器不够用的。这两个,开发调试都够了。关于支付宝的话,可以安装支付宝官方给的一个脚本就可以了。

邮件客户端: ThunderBird Mail 火狐家的,算比较好用的了。

笔记:为知笔记 ,为知真是业界良心,唯一一家提供linux版本的,做的还挺不错的,虽然有一些不好,但是相信以后会越做越好的。安装方法:

$ sudo add-apt-repository ppa:wiznote-team
$ sudo apt-get update
$ sudo apt-get install wiznote

常用编辑器:普通的编辑器就是用Vim,或者用sublime text,sb在ubuntu下中文输入会有问题,看这篇文章。http://blog.isming.me/blog/2014/03/15/jie-jue-ubuntuxia-sublime-text-3zhong-wen-shu-ru-de-wen-ti/.markdown 编辑器,我用retext,ubuntu仓库中有,直接下载就可以了。

开发工具:工欲善其事,必先利其器,作为android开发者,一个好用的工具也很重要,我用IntelliJ IDEA CE,同时配合Android SDK,推荐你们也用这个。

仓库:git,不解释。

抓包工具: windows 下面有很好用的工具Fiddler,在linux 下面我也找到一款好用的工具,Charles下载地址.

先写这么多了,以后有新发现,继续分享。

看完评论一下吧

Android消息循环分析

2014-04-02 19:14:57

我们的常用的系统中,程序的工作通常是有事件驱动和消息驱动两种方式,在Android系统中,Java应用程序是靠消息驱动来工作的。

消息驱动的原理就是:
1. 有一个消息队列,可以往这个队列中投递消息;
2. 有一个消息循环,不断从消息队列中取出消息,然后进行处理。
在Android中通过Looper来封装消息循环,同时在其中封装了一个消息队列MessageQueue。
另外Android给我们提供了一个封装类,来执行消息的投递,消息的处理,即Handler。

在我们的线程中实现消息循环时,需要创建Looper,如:

class LooperThread extends Thread {
	public Handler mHandler;
	public void run() {
		Looper.prepare(); //1.调用prepare
		......
		Looper.loop();	//2.进入消息循环
	}
}

看上面的代码,其实就是先准备Looper,然后进入消息循环。

  1. 在prepare的时候,创建一个Looper,同时在Looper的构造方法中创建一个消息队列MessageQueue,同时将Looper保存到TLV中(这个是关于ThreadLocal的,不太懂,以后研究了再说)
  2. 调用loop进入消息循环,此处其实就是不断到MessageQueue中取消息Message,进行处理。

然后再看我们如何借助Handler来发消息到队列和处理消息

Handler的成员(非全部):

final MessageQueue mQueue;    
final Looper mLooper;    
final Callback mCallback;    

Message的成员(非全部):

Handler target;            
Runnable callback;         

可以看到Handler的成员包含Looper,通过查看源代码,我们可以发现这个Looper是有两种方式获得的,1是在构造函数传进来,2是使用当前线程的Looper(如果当前线程无Looper,则会报错。我们在Activity中创建Handler不需要传Handler是因为Activity本身已经有一个Looper了),MessageQueue也就是Looper中的消息队列。

然后我们看怎么向消息队列发送消息,Handler有很多方法发送队列(这个自己可以去查),比如我们看sendMessageDelayed(Message msg, long delayMillis)

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
	if (delayMillis < 0) {    
		delayMillis = 0;    
	}
	return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);      
// SystemClock.uptimeMillis() 获取开机到现在的时间    
} 
	//最终所有的消息是通过这个发,uptimeMillis是绝对时间(从开机那一秒算起)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {    
	boolean sent = false;    
	MessageQueue queue = mQueue;    
	if (queue != null) {    
		msg.target = this;    
		sent = queue.enqueueMessage(msg, uptimeMillis);    
	}   
	return sent;   
}    

看上面的的代码,可以看到Handler将自己设为Message的target,然后然后将msg放到队列中,并且指定执行时间。

消息处理

处理消息,即Looper从MessageQueue中取出队列后,调用msg.target的dispatchMessage方法进行处理,此时会按照消息处理的优先级来处理:

  1. 若msg本身有callback,则交其处理;
  2. 若Handler有全局callback,则交由其处理;
  3. 以上两种都没有,则交给Handler子类实现的handleMessage处理,此时需要重载handleMessage。

我们通常采用第三种方式进行处理。

注意!!!!我们一般是采用多线程,当创建Handler时,LooperThread中可能还未完成Looper的创建,此时,Handler中无Looper,操作会报错。

我们可以采用Android为我们提供的HandlerThread来解决,该类已经创建了Looper,并且通过wait/notifyAll来避免错误的发生,减少我们重复造车的事情。我们创建该对象后,调用getLooper()即可获得Looper(Looper未创建时会等待)。

补充

本文所属为Android中java层的消息循环机制,其在Native层还有消息循环,有单独的Looper。并且2.3以后MessageQueue的核心向Native层下移,native层java层均可以使用。这个我没有过多的研究了!哈哈

PS:本文参考《深入理解Android:卷I》

看完评论一下吧

android异步操作总结

2014-03-16 16:18:25

Android中经常会有一些操作比如网络请求,文件读写,数据库操作,比较耗时,我们需要将其放在非UI线程去处理,此时,我们需要处理任务前后UI的变化和交互。我们需要通过类似js中异步请求处理,这里总结我所了解到的,方便自己记忆,也方便别人的浏览。

  1. AsyncTask

new AysncTask().execute();

AsyncTask会按照流程执行在UI线程和一个耗时的任务线程。

1.onPreExecute() 执行预处理,它运行于UI线程,可以为后台任务做一些准备工作,比如绘制一个进度条控件。

2.doInBackground(Params…) 后台进程执行的具体计算在这里实现,doInBackground(Params…)是AsyncTask的关键,此方法必须重载。在这个方法内可以使用publishProgress(Progress…)改变当前的进度值。

3.onProgressUpdate(Progress…) 运行于UI线程。如果在doInBackground(Params…) 中使用了publishProgress(Progress…),就会触发这个方法。在这里可以对进度条控件根据进度值做出具体的响应。

4.onPostExecute(Result) 运行于UI线程,可以对后台任务的结果做出处理,结果就是doInBackground(Params…)的返回值。此方法也要经常重载,如果Result为null表明后台任务没有完成(被取消或者出现异常)。

  1. Handler 创建Handler时需要传Lopper,默认是UI线程的。 通过Handler发送消息(Message)到主线程或者Handler的线程,

  2. Activity.runOnUiThread(Runnable) Runnable即可在UI线程执行

  3. View.post(Runnable) Runnable运行在UI线程 View.post(Runnable)方法。在post(Runnable action)方法里,View获得当前线程(即UI线程)的Handler,然后将action对象post到Handler里。在Handler里,它将传递过来的action对象包装成一个Message(Message的callback为action),然后将其投入UI线程的消息循环中。在Handler再次处理该Message时,有一条分支(未解释的那条)就是为它所设,直接调用runnable的run方法。而此时,已经路由到UI线程里,因此,我们可以毫无顾虑的来更新UI。

所有的异步操作原理本质都是通过Handler

基本上就这几种方法,当然也可自己使用消息循环常见类似的任务处理机制。

看完评论一下吧

解决Ubuntu下Sublime text 3中文输入的问题

2014-03-15 00:24:45

好久之前便听朋友说起Sublime Text这款软件很好用,终于这几天有空折腾,把软件给装起来了。用起来确实很不错,写代码很爽。
但是用了一段时间之后,我需要输入中文了,无论怎么切换输入法,都无法切换到中文。

网上搜索了一下,原来这是Bug。找解决方法吧。下面介绍我的解决方案,是大神cjacker解决成功的啦,我只是copy一下,方便大家在遇到这个问题的时候可以方便解决。

	我的系统:ubuntu 13.04  
	我的输入法:fcitx   
	sublime版本:3059    

理论上支持 sublime text2/3

1.保存代码sublime-imfix.c

/*
sublime-imfix.c
Use LD_PRELOAD to interpose some function to fix sublime input method support for linux.
By Cjacker Huang <jianzhong.huang at i-soft.com.cn>

gcc -shared -o libsublime-imfix.so sublime_imfix.c  `pkg-config --libs --cflags gtk+-2.0` -fPIC
LD_PRELOAD=./libsublime-imfix.so sublime_text
*/
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
typedef GdkSegment GdkRegionBox;

struct _GdkRegion
{
	long size;
	long numRects;
	GdkRegionBox *rects;
	GdkRegionBox extents;
};

GtkIMContext *local_context;

void
gdk_region_get_clipbox (const GdkRegion *region,
        GdkRectangle    *rectangle)
{	
	g_return_if_fail (region != NULL);
	g_return_if_fail (rectangle != NULL);

	rectangle->x = region->extents.x1;
	rectangle->y = region->extents.y1;
	rectangle->width = region->extents.x2 - region->extents.x1;
	rectangle->height = region->extents.y2 - region->extents.y1;
	GdkRectangle rect;
	rect.x = rectangle->x;
	rect.y = rectangle->y;
	rect.width = 0;
	rect.height = rectangle->height; 
	//The caret width is 2; 
	//Maybe sometimes we will make a mistake, but for most of the time, it should be the caret.
	if(rectangle->width == 2 && GTK_IS_IM_CONTEXT(local_context)) {
    	gtk_im_context_set_cursor_location(local_context, rectangle);
	}
}

//this is needed, for example, if you input something in file dialog and return back the edit area
//context will lost, so here we set it again.

static GdkFilterReturn event_filter (GdkXEvent *xevent, GdkEvent *event, gpointer im_context)
{
	XEvent *xev = (XEvent *)xevent;
	if(xev->type == KeyRelease && GTK_IS_IM_CONTEXT(im_context)) {
   		GdkWindow * win = g_object_get_data(G_OBJECT(im_context),"window");
   		if(GDK_IS_WINDOW(win))
     		gtk_im_context_set_client_window(im_context, win);
	}
	return GDK_FILTER_CONTINUE;
}

void gtk_im_context_set_client_window (GtkIMContext *context,
      GdkWindow    *window)
{
	GtkIMContextClass *klass;
	g_return_if_fail (GTK_IS_IM_CONTEXT (context));
	klass = GTK_IM_CONTEXT_GET_CLASS (context);
	if (klass->set_client_window)
	klass->set_client_window (context, window);

	if(!GDK_IS_WINDOW (window))
	return;
	g_object_set_data(G_OBJECT(context),"window",window);
	int width = gdk_window_get_width(window);
	int height = gdk_window_get_height(window);
	if(width != 0 && height !=0) {
		gtk_im_context_focus_in(context);
		local_context = context;
	}
	gdk_window_add_filter (window, event_filter, context); 
}

2.安装C/C++的编译环境和gtk libgtk2.0-dev

sudo	apt-get install build-essential
sudo apt-get install libgtk2.0-dev

3.编译共享内存

gcc -shared -o libsublime-imfix.so sublime_imfix.c  `pkg-config --libs --cflags gtk+-2.0` -fPIC

4.启动测试

LD_PRELOAD = ./libsublime-imfix.so sublime_text

正常的话这样是没有问题的。

然后我们在修改我们的desktop文件,使图标也可以使用

sudo vi /usr/share/applications/sublime-text.desktop

先将so文件移动到sublime text的目录

然后按照如下替换(主要是每次执行之前,去预加载我们的libsublime-imfix.so库)

[Desktop Entry]
Version=1.0
Type=Application
Name=Sublime Text
GenericName=Text Editor
Comment=Sophisticated text editor for code, markup and prose
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' %F
Terminal=false
MimeType=text/plain;
Icon=sublime-text
Categories=TextEditor;Development;
StartupNotify=true
Actions=Window;Document;

[Desktop Action Window]
Name=New Window
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' -n
OnlyShowIn=Unity;

[Desktop Action Document]
Name=New File
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' --command new_file
OnlyShowIn=Unity;

看完评论一下吧

Android图像开源视图:SmartImageView

2013-09-11 23:19:11

项目需要,开发中需要加载图片,自己要写图片从网上下载的方法,还要写缓存,等等。

在网上找到一个开源项目,smartImageVIew,支持从URL和通讯录中获取图像,可以替代Android标准的ImageView。

特征:

根据URL地址装载图像;
支持装载通讯录中的图像;
支持异步装载;
支持缓存;

这个是作者的项目主页,有使用方法。

http://loopj.com/android-smart-image-view/

下载作者的jar包导入项目后,在xml中加入控件

<com.loopj.android.image.SmartImageView android:id="@+id/my_image" />    

代码里找到该控件

SmartImageView myImage = (SmartImageView) this.findViewById(R.id.my_image); 

使用控件

通过url加载图片

myImage.setImageUrl("http://www.awesomeimages.com/myawesomeimage.jpg");   

加载通讯录的图片

myImage.setImageContact(contactAddressBookId);  

github上面有源码,需要的可以看看:

https://github.com/loopj/android-smart-image-view    

看完评论一下吧

Java中HashMap和HashTable的区别

2013-07-28 21:30:01

面试中遇到,但是不会,回来google到,分享下吧,据说是老掉牙的问题 HashMap 是Hashtable 的轻量级实现(非线程安全的实现),他们都完成了Map 接口,主要区别在于HashMap 允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。

HashMap 允许将null 作为一个entry 的key 或者value,而Hashtable 不允许。

HashMap 把Hashtable 的contains 方法去掉了,改成containsvalue 和containsKey。因为contains方法容易让人引起误解。

Hashtable 继承自Dictionary 类,而HashMap 是Java1.2 引进的Map interface 的一个实现。

最大的不同是,Hashtable 的方法是Synchronize 的,而HashMap 不是,在多个线程访问Hashtable 时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。

Hashtable 和HashMap 采用的hash/rehash 算法都大概一样,所以性能不会有很大的差异。

看完评论一下吧

Java中Collection和Collections的区别

2013-07-23 19:12:46

前几天去一个公司参加面试遇到这个问题,Java中Collection和Collections的区别,当时不会,回来从网上找到,现在记录一下。

1、java.util.Collection 是一个 集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack└Set

2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的 静态多态方法。此类 不能实例化 ,就像一个 工具类,服务于Java的Collection框架。

import java.util.ArrayList;      
import java.util.Collections;      
import java.util.List;      
public class TestCollections {      
    public static void main(String args[]) {     
        //注意List是实现Collection接口的       
        List list = new ArrayList();      
        double array[] = { 112, 111, 23, 456, 231 };      
        for (int i = 0; i < array.length; i++) {      
            list.add(new Double(array[i]));      
        }      
        Collections.sort(list);      
        for (int i = 0; i < array.length; i++) {      
            System.out.println(list.get(i));      
        }      
        // 结果:23.0 111.0 112.0 231.0 456.0      
    }      
}

看完评论一下吧

Linux下SVN的使用

2013-07-12 21:30:52

今天在新浪sae上搭建了个人博客,新浪sae采用svn的方式进行代码管理,之前在windows下面做svn操作都是采用TortoiseSVN,今天正好开机到了linux下面,那么好吧,就用svn传我的wordpress到sae中去。

首先,安装svn。

sudo apt-get install subversion

ubuntu下面安装svn,就是这么简单。

可以通过man svn查看使用手册。

下面介绍一些常用命令: 1.检出文件(checkout)。 使用命令:

svn co http://{svn repository url} /destination

然后系统会用当前的用户名登录,提示输入密码,如果第一次密码输入错误,会提示你输入用户名;输入正确后,就可以检出文件了。

2、Ubuntu SVN提交文件(commit)。 进入需要更新的目录,输入命令:

svn commit -m path-to-commit

,其中path-to-commit可以为空,成功后会提示更新后的版本号。

3、更新文件(update)。

svn update

,在要更新的目录运行这个命令就可以了。

4、查看日志(log)。

svn log path

5.删除文件

svn delete svn://xxx.com/test.php -m “delete test file”

或者直接svn delete test.php 然后再svn ci -m ‘delete test file‘,推荐使用这种简写:svn (del, remove, rm)

更多使用方法,自己查看手册吧。

看完评论一下吧

关于我

0001-01-01 08:00:00

网上主要使用两个名字“明明同学”和“码农明明桑”,使用同学是因为各个公司内都喊别人老师,只好谦逊的自称“同学”。“码农明明桑”最早是微博id,这个名字有点模仿“妈妈桑的意思”。

关于博客

从QQ空间和网易博客起接触博客,后又自己搭建wordpress,然而数次荒废,多次博客被废,最终选择了使用hugo静态托管。 先前博客存放于github pages,由于国内无法访问,后又托管于Vercel。由此每年的费用更低,维护更加简单,也更加容易坚持下来。 博客内容主要记录我的技术踩坑,个人随感,旅行记录等。

联系我

版权声明

本站文章遵循「CC BY-NC-SA 4.0(署名—非商业性使用—相同方式共享)」协议,转载请保留署名并注明来源。

本站作者照片同样遵循「CC BY-NC-SA 4.0」协议,如需使用,请保留水印并著名来源,如需大图,请联系作者。

你可以通过博客留言,或者公众号、邮箱与我联系。欢迎与我交流,共同进步成长!^_^

看完评论一下吧

互联网上的朋友们

0001-01-01 08:00:00

朋友们

博客导航

本站信息

标题: 码农明明桑
Url: https://isming.me
头像: https://isming.me/chrome-512.png
简介: 一心写码,无心务农

友链申请要求

交换友链可以发送邮件到[email protected] 或者在本页留言,博主看到后会及时处理。

申请友链需满足以下要求:

看完评论一下吧