面试中问到的知识点
GaoDun
- Volatile关键字原理:保证线程可见性
- 单例模式把getInstance()用synchronized修饰,在多线程中获取全局变量还需要加volatile关键字吗?
需要:示例代码如下
//单例
public class Singleton {
private boolean mFlag = false;//没有加volatile不能保证
//private volatile boolean mFlag = false;//加volatile保证可见性
private Singleton() {
}
private static Singleton sInstance;
public static synchronized Singleton getsInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
public void setFlag(boolean flag) {
this.mFlag = flag;
}
public boolean getFlag() {
return this.mFlag;
}
}
//测试类
public class TestSingleton {
public static void main(String[] args) {
Thread t1 = new Thread(new TRunnable("线程1"));
Thread t2 = new Thread(new TRunnable("线程2"));
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程3结束任务标志位");
Singleton.getsInstance().setFlag(true);
}
});
t1.start();
t3.start();
t2.start();
}
static class TRunnable implements Runnable {
String name;
public TRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("--------" + name + " 开始--------");
while (!Singleton.getsInstance().getFlag()) {
System.out.println(name + "执行任务。。。");
}
System.out.println("--------" + name + " 开始--------");
}
}
}
//输出结果:线程3执行了标志位,线程2还会执行任务,单例加锁不能保证可见性
--------线程1 开始--------
--------线程2 开始--------
线程3结束任务标志位
线程2执行任务。。。
--------线程2 开始--------
线程1执行任务。。。
--------线程1 开始--------
//
- 在单例多线程中是否可以不用给全局变量修饰
- 认为自己最大的安卓这块的优势
- 深度没有有,但是要钻研,没有总结,现在所在的项目,都是做好的,没有挑战性。要钻研问题
- MVVM的使用
- Rxjava的使用
- 弱引用的使用,handler中示例,为啥用弱引用
- 考研经历,怎么上的
- 怎么实现的账号检测
- Okhttp的几种常用拦截器
- Android target作用,会有啥影响
- 自己做的模块怎么分层设计:model,view,controler这种
- Kotlin多不多
- Mvp的各个部分用处
- 场景题:mvp如果A页面,
- 网络请求发出去怎么结束掉,能结束掉吗
- Rxjava中observer怎么和页面绑定,订阅者
DingDongMaiCai
- Retofite
- JNI接触过吗
- Glide
- Android 9和Android 10适配
- 图片加载库
- 网络这块用的
- Acticity生命周期
- 如果后台按下去,内存不足,onsaveunstance
- Livedata相关
- Jetpack用过吗
- Okhttp里面
- SessionCookie用过吗
- 网络请求有加密吗
- 职业规划发展,走研发路线还是管理
HONOR
- 四大组件
- Activity生命周期
- 支付的流程
- 最近项目遇到困难
- View Touch事件的点击
public boolean dispatchTouchEvent(MonthinEvent e) {
boolean consumed = false;//事件是否被消费
if(onInterceptTouchEvent(e)) {//如果自己拦截下事件
consumed = onTouchEvent(e);//交给自己处理
} else {
consumed = child.dispatchTouchEvent(e);//交给孩子继续分发
}
return consumed;//返回消费结果,ture表示已经消费,false表示交给父的onTouchEvent处理
}
Shanghai TikTok
- 屏幕适配: 字节适配方案
- 类加载的过程
JVM运行时内存分布
-
创建一个对象的方法有几种
- new对象
One one = new One();- 通过 Class 类的 newInstance() 方法,调用类的默认无参构造函数
1. Class<One> one= One.class; One oneObjcet = one.newInstance(); 2. Person p = (Person) Class.forName("com.bean.Person").newInstance();- 反射Constructor
Class<?> oneClz = Class.forClass("com.my.One"); oneClz.newInstance();- clone()对象
One one = (One) origin.clone();- 序列化和反序列化
序列化 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("D:/objectUser.txt")); oos.writeObject(); oos.close(); 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/objectUser.txt")); User user = (User)ois.readObject();- 总结示例
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, CloneNotSupportedException { //1 new 对象 Person p1 = new Person(); //2 newInstance() Person p2 = (Person) Class.forName("interview.instance.Person").newInstance(); //3 Constructor Person p3 = (Person) Person.class.getConstructors()[0].newInstance(); //4 Clone方法 Person p4 = (Person) p3.clone(); //5 反序列化 }
- 为什么要有双亲委派
- hashCode的作用
- 如何知道哪个类已经被加载了
- HandlerThread作用
- ThreadLocal作用
- 能不能修改系统加载的类,替换掉如String类
- sp如何实现加锁的
- 拦截器解析
- okhttp实现统计请求时间的拦截器
- 拦截器分为Appliction拦截器和NetWork拦截器
- 常用拦截器:RetryAndFollowUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor, CallServerInteceptor
- Android中有哪些跨进程的实现方式
- 模块开发中如何实现两个子moudle的通信:桥
- HashMap的原理
- HashMap是线程安全的吗
- CuccreentMap如何实现线程安全的
- 账号泄露检测中如何保证账号的安全,用什么加密
- 如果从新设计,如何保证,对称加密实现
- 从new一个对象开始,如果的过程
- java中多线程会有什么问题
- sqlite和sp的区别和为啥用
- View中的MeasureSpec的size如何确定的
- sqlite的实现
- HashTable和HashMap区别
- HashMap不是线程安全,HashTable是线程安全,方法加了Synchronize
- HashMap的Key和Value允许null值,HashTable不允许
- HashMap去掉contains()方法,改为containKey()和containValue(),HashTable是contains()
- 父类不同,HashTable是继承Dictionary,HashMap是继承AbstractMap实现Map接口
- 都实现hash算法
- 什么情况用sqlite
- sp为什么不能存大的数据
- 如何遍历一个二叉查找树,并把结果输出成有序
- 检查一个树是否是二叉搜索树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isValidBST(TreeNode *root) {
if(!root) return true;
return (isValidBST(root->left) && isValidVal(root->val) && isValidBST(root->right)); //中序遍历
}
private:
bool isValidVal(int val) {
if(bFirstNode) {
bFirstNode = false;
prevNodeVal = val;
return true;
}
if(prevNodeVal >= val) return false;
prevNodeVal = val;
return true;
}
bool bFirstNode = true;
int prevNodeVal = 0; //记录前一个结点的值
};
- 根据递增排序的序列生成二权搜索树
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode *sortedListToBST(ListNode *head) {
list = head;
return listToBST(getListLength(head));
}
private:
int getListLength(ListNode *node) {
int length = 0;
while(node) {
++length;
node = node->next;
}
return length;
}
TreeNode *listToBST(int n) { //中序遍历
if(n == 0) return NULL;
TreeNode *pNode = new TreeNode(0);
pNode->left = listToBST(n / 2); //左子树共(n / 2)个结点
pNode->val = list->val;
list = list->next;
pNode->right = listToBST(n - n / 2 - 1); //右子树共(n - n / 2 - 1)个结点
return pNode;
}
ListNode *list; //记录当前位置
};
将有序数组转换为二叉搜索树 中序遍历二叉搜索树即可
Bilibili
- 原子,一致,可见行 *
- 单例模式几种创建的模式,优缺点
- http1/http2区别
- 最大区别是多路复用,2.0请求和响应都是发送是二进制帧,
- RxJava特点
- OKHttp的多线程实现网络请求
- OKHttplanjieq
- RetryAndFollowUpInterceptor: 做网络连接失败重试
- BridgeInterceptor:初始化信息,添加头,gzip,keep-alive,返回reponse解压
- CacheInterceptor:处理缓存操作
- ConnectionInterceptor:建立链接
- CallServerInterceptor:真正连接服务器发送并返回服务器数据,http写入数据流,并从io流中读取数据返回客户端
- 线程池创建的方式,最重要的参数排序
- 数组组最大的整数
- 快排,冒泡排序
- volitate关键字原理
- sychronized关键字原理
- 用的Android框架
- 单例模式
- 单例模式注意点
- 懒汉模式双判空的原因
public class Singleton {
private Singleton() {
}
///volatile防止指令重排
private volatile static Singleton sIntance;
public static Singleton getInstance() {
//当多个线程同时调用单例获取方法,先判断当前实例是否创建,已创建直接返回实例
if(sIntance == null) {
//当没有创建实例,如果此时有多个线程:A和B同时抢锁去创建实例,
//获得锁的线程才能创建
synchronized(Singleton.class) {
//第二步再次判空,因为比如A线程先抢到锁,B线程在外边等待锁,A进入代码块创建实
//例了,然后A线程释放锁,此时B线程获得锁,加入不再次判断实例是否为空,那么刚
//A线程创建了实例,B此时进入后又创建了一个实例,不满足单例了,必须在进入同步
//代码块时候,再次判断实例是否为空,不为空再创建
if(sIntance == null) {
sIntance = new Singleton();
}
}
}
return sIntance;
}
}
* 优点:懒加载,不用时候节约内存空间。
* 缺点:不加锁时候有线程安全问题,可能多个线程多个初始化实例,加锁造成程序串行化,造成同步性能问题。
- 饿汉模式
public class Singleton {
private static final Singleton sInstance= new Singleton();
private Singleton(){}
public static getInstance(){
return sInstance;
}
}
* 优点:因为使用了static关键字,保证了调用时候,关于这个变量的所有写操作都加载完成,在jvm层面保证了线程安全。
* 缺点:不能实现懒加载,如果初始化时候就加载了整个类,但是很长时间没有使用,那么造成内存空间的浪费。
- 静态内部类
public class Singleton {
//私有构造函数
private Singleton() {
}
private static class InstanceHolder {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.instance;
}
}
* 特点:在没有加锁情况下保证线程安全,并且不会造成空间的浪费
* 优点:可以实现懒加载,加载Singleton类时候并没有加载静态内部类
* 缺点:无法防止反射实例化
- 枚举单例:枚举类型是线程安全的,并且只会装载一次
public class Singleton {
private Singleton(){}
private enum SingletonHolder {
INSTANCE;
private final Singleton instance;
SingletonHolder(){
instance = new Singleton();
}
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE.instance;
}
}
* 优点:防止反射实例化
* 缺点:无法实现懒加载
TianYanCha
activity与fragment区别
- 生命周期区别:Fragment比Activity灵活,因为可以控制的生命周期多
- Activity生命周期:onCreate()-onStart()-onResume()-onPause()-onStop()-onDestroy()
- Fragment生命周期:onAttach()-onCreate()-onCreateView()-onActivityCreate()-onStart()-onResumt()-显示出-onPause()-onStop()-onDestroyView()-onDestroy()-onDetach()
- Activity生命周期:onCreate()-onStart()-onResume()-onPause()-onStop()-onDestroy()
- 灵活性来说:Fragment更灵活,Fragment可以在xml中定义,也可以在代码里面添加;fragment的替换replace()或者show()或者hide(),切换的时候不会有明显效果,activity的切换会有明显效果影响体验。
自定义view要重写函数
- 自定义view详解
- 自定义view几种方式:
- 自定义组合控件,几个控件组合成一个,在多处使用
- 继承系统View控件,如继承系统TextView并且扩展其功能
- 继承View控件,重写onMesure() onDraw()方法
- 继承系统ViewGroup控件,如RelativeLayout并改写里面的方法
- 继承ViewGroup控件,重写onMeasure(),onLayout(),onDraw()方法
- 自定义View | 函数 | 作用 | 相关方法 | |---|---|---| |measure| 测量view宽高|onMeasure(),setMeasuredDimension()| |layout|布局控件|onLayout(),setFrame()| |draw|绘制|onDraw()|
自定义view有几个构造方法 5种构造方法
- View()
View() {
mResources = null;
mRenderNode = RenderNode.create(getClass().getName(), this);
}
- View(Context context)
public View(Context context) {
}
- View(Context context, AttributeSet attrs)
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
- View(Context context, AttributeSet attrs, int defStyleAttr)
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
- public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
}
自定义view一般都重写三个构造方法
public StarView(Context context) {
super(context);
}
public StarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public StarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs == null) {
return;
}
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NCRippleViewStyleable);
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
mMinRadius = ta.getDimension(R.styleable.NCRippleViewStyleable_nc_ripple_min_radius, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_RADIUS, displayMetrics));
mMaxRadius = ta.getDimension(R.styleable.NCRippleViewStyleable_nc_ripple_max_radius, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MAX_RADIUS, displayMetrics));
mMinStroke = ta.getFloat(R.styleable.NCRippleViewStyleable_nc_ripple_min_stroke, DEFAULT_MIN_STROKE);
mMaxStroke = ta.getFloat(R.styleable.NCRippleViewStyleable_nc_ripple_max_stroke, DEFAULT_MAX_STOKE);
mColor = ta.getColor(R.styleable.NCRippleViewStyleable_nc_ripple_color, DEFAULT_COLOR);
}
自定义view如果加载xml布局要哪个构造方法
- 重写两个参数的
public View(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
View绘制过程
Activity启动和触摸事件传递
- Activity创建和WindowInputEvnetReciver创建
Activity启动过程
- ActivityThread.performLaunchActivity() ->
- Activity通过反射方式创建出activity对象:
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
-> AppComponentFactory.instantiateActivity(ClassLoader,className,intent)
-> (Activity) cl.loadClass(className).newInstance()
- Activity创建后执行attach()方法 -> 创建PhoneWindow -> 并把当前Activity作为Callback赋给PhoneWindow
- ActivityThread.handleResumeActivity() -> performResumeActivity()获取ActivityClientRecord:包含具体activity对象
- ActivityThread.handleResumeActivity() -> Activity.mDecor = Activity.window.getDecorView()将PhoneWindow的decorview赋值给activity的mDecor属性
- Activity.window.getDecorView() -> 执行DecorView.installDecor(),创建decorview -> activity中View是 DecorView, activity中mWindow是 PhoneWindow
- Activity执行setVisible() -> Activity.makeVisible() -> WindowManager.addView()
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
- WindowManager.addView() -> 调用WindowManagerImpl.addView() -> WindowManagerGlobal.addView() -> 创建ViewRootImpl
WindowManagerGlobal.
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);//view为decorview
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);//
} catch (RuntimeException e) {
...
throw e;
}
}
- ViewRootImpl.setView() -> requestLayout() -> shceduleTraversals() -> doTraversal() -> performTraversals()
- 关键performTraversals() -> perfromMeasure() -> performLayout() -> performDraw()
// =====================ViewRootImpl.java=================
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, mWidth, mHeight);
......
performDraw();
}
- View创建和绘制流程图
算法合并两个数组
/**
* m为a数组中元素个数
* n为b数组中元素个数
*/
public void merge(int a[],int b[], int m, init n) {
int tail = a.length -1;
int p1 = m -1 ;
int p2 = n -1;
while( p1 > 0 && p2 > 0) {
if(a[p1] > b[p2]) {
a[tail] = a[p1];
p1--;
} else {
a[tail] = b[p2];
p2--;
}
tail--;
}
while(p1 > 0) {
a[tail] = a[p1];
p1--;
}
while(p2 > 0) {
a[tail] = b[p2];
p2--;
}
}
flutter为啥要用redux
randerobject和elment还有widget关系
stateful和stateless widget的区别
stringbuilder和string区别
什么场景用stringbuidler,内存或者实现原理上面说
横竖屏切换时,activity生命周期
- AndroidManifest.xml不添加配置
onPause
onStop
onDestroy
onCreate
onStart
onRestoreSaveInstance
onResume
- AndroidManifest.xml添加android:configChanges="orientation|screenSize"
切换横屏:
onConfigChanged()
- AndroidManifest.xml添加android:configChanges="orientation|screenSize|keyboardHidden"
切换横屏:
onConfigChanged()
切换竖屏:
onConfigurationChanged()
- 设置Activity的android:configChanges="orientation|keyboardHidden|screenSize"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法
- 当前Activity和弹窗系统AlertDialog时候,activity不会有生命周期变化
- 按下Home键:
onPause -> onStop
再切回去:
onRestart -> onStart -> onResume
Anroid基础知识
HashCode的作用
- hashCode作用解析
- 通俗解释hashCode和equals:
1.hashCode是用来查找的,比如当前系统内存空间:[1],[2],[3],[4],[5]五个区域可以存放对象,
如果将对象放在内存中后,想要查找每个对象,比如p1放在内存[3]上,查找的话必须顺序遍历内存地址挨
个查找或者二分法查找。
2.如果使用hashCode,存对象的时候获取对象的hashCode,比如对象有个属性ID,每次存对象的时候将ID
与内存空间5取余:id % 5 = 存放的内存地址,那么查找时候只要直接获取对象hashCode并且直接与空间
取余就可以在O(1)时间内找到对象。
3.但是如果有两个对象存的地址相同,那么就需要用到equals来对比两个对象是否相同。例如:对象1 id为8,
对象2 id为3,对象1存放地址:8 % 5 = 3,对象2存放地址:3 % 5 =3。相当于放在同一个桶里面,去取对象
的时候就需要重写equals来对比俩对象是否相同,找到我们要找的对象。
4.为何重写equals后必须要重写hashCode,因为如果不重写hashCode,就找不到对象存放的那个桶,只写equals
没用
- 示例: 只重写hashCode时候
public static void main(String[] args) {
HashSet<TObject1> set = new HashSet<>();
TObject1 p1 = new TObject1(2);
TObject1 p2 = new TObject1(2);
set.add(p1);
set.add(p2);
System.out.println(p1.hashCode() == p2.hashCode());
System.out.println(p1.equals(p2));
System.out.println(set);
}
//测试类1
static class TObject1 {
int val;
public TObject1(int val) {
this.val = val;
}
@Override
public int hashCode() {
return Objects.hash(val);
}
}
结果:
true //两个对象hashCode相同
false //两个对象不是同一个对象,不是内存中同一个对象
[interview.hashcode.HashCodeTest$TObject1@21, interview.hashcode.HashCodeTest$TObject1@21]//hashSet判断
重写hashCode和equals方法
public static void main(String[] args) {
HashSet<TObject1> set = new HashSet<>();
TObject1 p1 = new TObject1(2);
TObject1 p2 = new TObject1(2);
set.add(p1);
set.add(p2);
System.out.println(p1.hashCode() == p2.hashCode());
System.out.println(p1.equals(p2));
System.out.println(set);
}
//测试类2
static class TObject1 {
int val;
public TObject1(int val) {
this.val = val;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null) {
return false;
}
if (!(o instanceof TObject1)) {
return false;
}
TObject1 to = (TObject1) o;
if (to.hashCode() == this.hashCode() && to.val == this.val) {
return true;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(val);
}
}
结果:
true
true
[interview.hashcode.HashCodeTest$TObject1@21]
ArrayList和LinkedList区别
- 共同点:都是继承List的
- ArrayList基于数组的,LinkedList基于链表的
- ArrayList数据都是放在一块内存地址的,而LinkedList是放在不同位置
- 插入和删除数据是LinkedList更快,只用移动指针即可,而查找数据是ArrayList更快
- 扩容区别,每次ArrayList都是将数组元素拷贝扩容:实际是System.arrayCopy();
- LinkedList是双端队列,可以实现栈功能,队列和双端队列功能
- ArrayList
- LinkedList
- 对比总结
ThreadLocal
- 作用:适用于某些数据以线程为作用域,并且不同线程具有不同数据副本的场景。
- 内部类ThreadLocalMap
- 是一个数组
- 实现线程间数据不扰乱,原理:将线程作为key,存入ThreadLocal时候key-value键值对,所以不同线程不会出现操作同一个entry情况
计算hash方式
- 随机数法
- 斐波那契散列法
- 平方散列法
- 除法散列法
voliate的作用
- 原理:
1. 添加voliate关键字的变量会在编译时候加:ACC_VOLIATE flag修饰
2. 汇编指令:lock addl $0x0,(%rsp),也就是 lock 的前缀指令
3. lock前缀指令,内存屏障:
* 将本处理器缓存刷新到内存中
* 重排序时候不能把后边指令重新排序到内存屏障前面
* 对内存写入动作会导致其他的处理器中对应内存无效
- 并不能解决原子性问题,需要用到synchronized或者lock
- synchronized保证原子性,voliate保证可见性
- 保证内存可见性:每次读写遍历从主内存读取,保证线程间访问的变量及时更新
- 控制被修饰的变量在内存上的操作主动刷新到主内存中,
- 禁止指令重排
synchronized同步
Android内存泄露几种情况
- Handler使用不当导致: 未定义静态内部类,当activity结束时候,还有消息发送到messagequeue队列,此时如果页面销毁,messagequeue中消息未处理完,hanlder还持有activity的引用,此时message和handler被messagequeue持有,导致内存泄露
- 静态变量导致:静态变量在类加载时候初始化
- 资源文件,流等使用后未关闭导致的内存泄露
- 集合类中对象只增不减,不删减会导致
- 非静态内部类
工厂模式几种写法
-
简单工厂模式:一个工厂,传入不同的参数type生产对应的对象,对象有公共的父类
-
工厂模式:不同的工厂buidler,每个工厂生产对应的对象
-
抽象工厂模式:一个工厂生产多个对应类型的对象,当只生产一个对象时候,就是工厂模式,当生产多个对象时候,是抽象工厂模式
-
工厂模式是面向一个产品等级,如工厂只要生成鼠标;而抽象工厂是面向多个产品等级:如一个工厂需要生成耳机,键盘,鼠标
工厂模式针对的是一个产品等级结构 ,抽象工厂模式针对的是面向多个产品等级结构的。