面试复习笔记:
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《960页Android开发笔记》
《1307页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
接下来,让我们一起来看一下怎样 Hook View.OnClickListener 事件?
大概分为三步:
- 第一步:获取 ListenerInfo 对象
从 View 的源代码,我们可以知道我们可以通过 getListenerInfo 方法获取,于是,我们利用反射得到 ListenerInfo 对象
- 第二步:获取原始的 OnClickListener事件方法
从上面的分析,我们知道 OnClickListener 事件被保存在 ListenerInfo 里面,同理我们利用反射获取
- 第三步:偷梁换柱,用 Hook代理类 替换原始的 OnClickListener
public static void hookOnClickListener(View view) throws Exception {
// 第一步:反射得到 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod(“getListenerInfo”);
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 第二步:得到原始的 OnClickListener事件方法
Class<?> listenerInfoClz = Class.forName(“android.view.View$ListenerInfo”);
Field mOnClickListener = listenerInfoClz.getDeclaredField(“mOnClickListener”);
mOnClickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
// 第三步:用 Hook代理类 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
public class HookedClickListenerProxy implements View.OnClickListener {
private View.OnClickListener origin;
public HookedClickListenerProxy(View.OnClickListener origin) {
this.origin = origin;
}
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), “Hook Click Listener”, Toast.LENGTH_SHORT).show();
if (origin != null) {
origin.onClick(v);
}
}
}
执行以下代码,将会看到当我们点击该按钮的时候,会弹出 toast “Hook Click Listener”
mBtn1 = (Button) findViewById(R.id.btn_1);
mBtn1.setOnClickListener(this);
try {
HookHelper.hookOnClickListener(mBtn1);
} catch (Exception e) {
e.printStackTrace();
}
简单案例二: HooK Notification
发送消息到通知栏的核心代码如下:
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(id, builder.build());
跟踪 notify 方法发现最终会调用到 notifyAsUser 方法
public void notify(String tag, int id, Notification notification)
{
notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}
而在 notifyAsUser 方法中,我们惊喜地发现 service 是一个单例,因此,我们可以想方法 hook 住这个 service,而 notifyAsUser 最终会调用到 service 的 enqueueNotificationWithTag 方法。因此 hook 住 service 的 enqueueNotificationWithTag 方法即可
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
//
INotificationManager service = getService();
String pkg = mContext.getPackageName();
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != null) {
notification.sound = notification.sound.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
notification.sound.checkFileUriExposed(“Notification.sound”);
}
}
fixLegacySmallIcon(notification, pkg);
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "
- notification);
}
}
if (localLOGV) Log.v(TAG, pkg + “: notify(” + id + ", " + notification + “)”);
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static INotificationManager sService;
static public INotificationManager getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService(“notification”);
sService = INotificationManager.Stub.asInterface(b);
return sService;
}
综上,要 Hook Notification,大概需要三步:
- 第一步:得到 NotificationManager 的 sService
- 第二步:因为 sService 是接口,所以我们可以使用动态代理,获取动态代理对象
- 第三步:偷梁换柱,使用动态代理对象 proxyNotiMng 替换系统的 sService
于是,我们可以写出如下的代码
public static void hookNotificationManager(final Context context) throws Exception {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Method getService = NotificationManager.class.getDeclaredMethod(“getService”);
getService.setAccessible(true);
// 第一步:得到系统的 sService
final Object sOriginService = getService.invoke(notificationManager);
Class iNotiMngClz = Class.forName(“android.app.INotificationManager”);
// 第二步:得到我们的动态代理对象
Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
Class[]{iNotiMngClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, “invoke(). method:” + method);
String name = method.getName();
Log.d(TAG, “invoke: name=” + name);
if (args != null && args.length > 0) {
for (Object arg : args) {
Log.d(TAG, “invoke: arg=” + arg);
}
}
Toast.makeText(context.getApplicationContext(), “检测到有人发通知了”, Toast.LENGTH_SHORT).show();
// 操作交由 sOriginService 处理,不拦截通知
return method.invoke(sOriginService, args);
// 拦截通知,什么也不做
// return null;
// 或者是根据通知的 Tag 和 ID 进行筛选
}
});
// 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 sService
Field sServiceField = NotificationManager.class.getDeclaredField(“sService”);
sServiceField.setAccessible(true);
sServiceField.set(notificationManager, proxyNotiMng);
}
Hook 使用进阶
Hook ClipboardManager
第一种方法
从上面的 hook NotificationManager 例子中,我们可以得知 NotificationManager 中有一个静态变量 sService,这个变量是远端的 service。因此,我们尝试查找 ClipboardManager 中是不是也存在相同的类似静态变量。
查看它的源码发现它存在 mService 变量,该变量是在 ClipboardManager 构造函数中初始化的,而 ClipboardManager 的构造方法用 @hide 标记,表明该方法对调用者不可见。
而我们知道 ClipboardManager,NotificationManager 其实这些都是单例的,即系统只会创建一次。因此我们也可以认为
ClipboardManager 的 mService 是单例的。因此 mService 应该是可以考虑 hook 的一个点。
public class ClipboardManager extends android.text.ClipboardManager {
private final Context mContext;
private final IClipboard mService;
/** {@hide} */
public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
mContext = context;
mService = IClipboard.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
}
}
接下来,我们再来一个看一下 ClipboardManager 的相关方法 setPrimaryClip , getPrimaryClip
public void setPrimaryClip(ClipData clip) {
try {
if (clip != null) {
clip.prepareToLeaveProcess(true);
}
mService.setPrimaryClip(clip, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
- Returns the current primary clip on the clipboard.
*/
public ClipData getPrimaryClip() {
try {
return mService.getPrimaryClip(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
可以发现这些方法最终都会调用到 mService 的相关方法。因此,ClipboardManager 的 mService 确实是一个可以 hook 的一个点。
hook ClipboardManager.mService 的实现
大概需要三个步骤
- 第一步:得到 ClipboardManager 的 mService
- 第二步:初始化动态代理对象
- 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
public static void hookClipboardService(final Context context) throws Exception {
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
Field mServiceFiled = ClipboardManager.class.getDeclaredField(“mService”);
mServiceFiled.setAccessible(true);
// 第一步:得到系统的 mService
final Object mService = mServiceFiled.get(clipboardManager);
// 第二步:初始化动态代理对象
Class aClass = Class.forName(“android.content.IClipboard”);
Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
Class[]{aClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(TAG, “invoke(). method:” + method);
String name = method.getName();
if (args != null && args.length > 0) {
for (Object arg : args) {
Log.d(TAG, “invoke: arg=” + arg);
}
}
if (“setPrimaryClip”.equals(name)) {
Object arg = args[0];
if (arg instanceof ClipData) {
ClipData clipData = (ClipData) arg;
int itemCount = clipData.getItemCount();
for (int i = 0; i < itemCount; i++) {
ClipData.Item item = clipData.getItemAt(i);
Log.i(TAG, “invoke: item=” + item);
}
}
Toast.makeText(context, “检测到有人设置粘贴板内容”, Toast.LENGTH_SHORT).show();
} else if (“getPrimaryClip”.equals(name)) {
Toast.makeText(context, “检测到有人要获取粘贴板的内容”, Toast.LENGTH_SHORT).show();
}
// 操作交由 sOriginService 处理,不拦截通知
return method.invoke(mService, args);
}
});
// 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
Field sServiceField = ClipboardManager.class.getDeclaredField(“mService”);
sServiceField.setAccessible(true);
sServiceField.set(clipboardManager, proxyInstance);
}
第二种方法
总结
首先是感觉自己的基础还是不够吧,大厂好像都喜欢问这些底层原理。
另外一部分原因在于资料也还没有看完,一面时凭借那份资料考前突击恶补个几天居然也能轻松应对(在这里还是要感谢那份资料,真的牛),于是自我感觉良好,资料就没有怎么深究下去了。
之前的准备只涉及了Java、Android、计网、数据结构与算法这些方面,面对面试官对其他基础课程的考察显得捉襟见肘。
下一步还是要查漏补缺,进行针对性复习。
最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
.(img-OmmsKYpt-1715892813672)]
[外链图片转存中…(img-3jFzHtG4-1715892813672)]
[外链图片转存中…(img-Zabkopxf-1715892813672)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!