美团App页面视图可测性改造实践

 ****


一次编写多处运行的动态化容器技术给研发效率带来了极大的提升,但对于依旧需要多端验证的测试流程来说,在效率层面却面临着极大的挑战。本文围绕动态化容器中的动态布局技术,阐述了如何通过可测性改造来帮助达成提升测试效率的目标。希望可以给同样需要测试动态化页面的同学们带来一些启发和帮助。


  * 美团App的页面特点


  * 自动化测试实施中的技术挑战


    * 页面元素无法定位


    * Appium元素定位的原理


    * AccessibilityNodeInfo和Drawable


  * 页面视图可测性改造-XraySDK


    * 定位方案对比


    * 视图信息的获取和存储-XrayDumper


    * 视图信息的输出-XrayServer


    * SDK整体功能结构


    * 视图信息的增强


    * 动态布局自动化的收益


  * 未来展望


    * 使用视图解析原理解决WebView元素定位


    * 视图可测性改造更多的应用场景


美团App的页面特点  


对于不同的用户,美团App页面的呈现方式其实多种多样,这就是所谓的“千人千面”。以美团首页的“猜你喜欢”模块为例,针对与不同的用户有单列、Tab、双列等多种不同形式。这么多不同的页面样式需求,如果要在1天内时间内完成开发、测试、上线流程,研发团队也面临着很大的挑战。所以测试工程师就需要重度依赖自动化测试来形成快速的验收机制。


图1 美团App首页多种页面布局样式


## 自动化测试实施中的技术挑战


接下来,本文将会从页面元素无法定位、Appium元素定位的原理、AccessibilityNodeInfo和Drawable等三个维度进行阐述。


### 页面元素无法定位


图2 页面元素审查情况


目前,美团App客户端自动化主要依托于Appium(一个开源、跨平台的测试框架,可以用来测试原生及混合的移动端应用)来实现页面元素的定位和操作,当我们通过Appium

Inspector进行页面元素审查时,能通过元素审查找到的信息只有外面的边框和下方的两个按钮,其他信息均无法识别(如上图2所示)。中央位置的图片、左上角的文本信息都无法通过现有的UI自动化方案进行定位和解析。不能定位元素,也就无法进行页面的操作和断言,这就严重影响了自动化的实施工作。


经过进一步的调研,我们发现这些页面卡片中大量使用Drawable对象来绘制页面的信息,从而导致元素无法进行定位。为什么Drawable对象无法定位呢?下面我们一起研究一下UI自动化元素定位的原理。


### Appium元素定位的原理


目前的UI自动化测试,使用Appium进行页面元素的定位和操作。如下图所示,AppiumServer和UiAutomator2的手机端进行通信后完成元素的操作。


图3 Appium的通信原理


通过阅读Appium源码发现完成一次定位的流程如下图所示:


图4 Appium定位元素的实现流程


  * 首先,Appium通过调用`findElement`的方式进行元素定位。

  * 然后,调用Android提供`UIDevice`对象的`findObject`方法。

  * 最终,通过`PartialMatch.accept`完成元素的查找。


接下来我们看一下,这个`PartialMatch.accept`到底是如何完成元素定位的。通过对于源码的研究,我们发现元素的信息都是存储在一个叫做`AccessibilityNodeInfo`的对象里面。源码中使用大量`node.getXXX`方法中的信息,大家是否眼熟呢?这些信息其实就是我们日常自动化测试中可以获取UI元素的属性。


图5 AppiumInspector审查元素获取信息示意


`Drawable`无法获取元素信息,是否和`AccessibilityNodeInfo`相关?我们进一步探究`Drawable`和`AccessibilityNodeInfo`的关系。


### AccessibilityNodeInfo和Drawable


通过对于源码的研究,我们绘制了如下类图来解释`AccessibilityNodeInfo`和`Drawable`之间的关系。


图6 类关系示意图


`View`实现了`AccessibilityEventSource`接口并实现了一个叫做`onInitializeAccessibilityNodeInfo`的方法来填充信息。我们也在Android官方文档中找到了对于此信息的说明:


> onInitializeAccessibilityNodeInfo()

> :此方法为无障碍服务提供有关视图状态的信息。默认的`View`实现具有一组标准的视图属性,但如果您的自定义视图提供除了简单的

> `TextView`或`Button`之外的其他互动控件,则您应替换此方法并将有关视图的其他信息设置到由此方法处理的`AccessibilityNodeInfo`对象中。


而`Drawable`并没有实现对应的方法,所以也就无法被自动化测试找到。探究了元素查找原理之后,我们就要开始着手解决问题了。


## 页面视图可测性改造-XraySDK


### 定位方案对比


既然知道了`Drawable`没有填充`AccessibilityNodeInfo`,也就说明我无法接入目前的自动化测试方案来完成页面内容的获取。那我们可以想到如下三种方案来解决问题:


实现方案| 影响范围  

---|---  

改造Appium定位方式,让Drawable可以被识别| 需要改动底层的AccessibilityNodeInfo

obtain(View,int)方法和为Drawable添加AccessibilityNodeInfo这样就需要对于所有的Android系统做兼容,影响范围过大  

使用View替代Drawable|

动态布局卡片使用Drawable进行绘制就是因为Drawable比View使用资源更少,绘制性能更好,放弃使用Drawable就等于放弃了性能的改进  

使用图像识别进行定位| 动态卡片中有很多图像中包含文字,还有多行文本都会对图像识别的准确性带来很大的影响  


上面的三种方案,目前看来都无法有效地解决动态卡片元素定位的问题。如何在影响范围较小的前提下,达成获取视图信息的目标呢?接下来,我们将进一步研究动态布局的实现方案。


### 视图信息的获取和存储-XrayDumper


我们的应用场景非常明确,自动化测试通过集成Client来获得和客户端交互能力,通过Client向App发送指令来页面信息的获取。那我们可以考虑内嵌一个SDK(XraySDK)来完成视图的获取,然后再向自动化提供一个客户端(XrayClient)来完成这部分功能。


图7 XraySDK的工作流程示意图


对于XraySDK的功能划分,如下表所示:


模块名| 功能划分| 运行环境| 产品形态  

---|---|---|---  

Xray-Client| 1.和Xray-Server进行交互进行指令发送和数据的接收  

2.暴露对外的Api给自动化或者其他系统| App内部| 客户端SDK(AAR和Pod-Library)  

Xray-SDK| 1.进行页面信息的获取以及结构化(Xray-Dumper)  

2.接收用户指令来进行结构化数据输出(Xray-Server)| 自动化内部或者三方系统内部| JAR包或基于其他语言的依赖包  


XraySDK如何才能获取到我们需要的Drawable信息呢?我们先来研究一下动态布局的实现方案。


图8 动态卡片的页面绘制流程


动态布局的视图呈现过程分为:解析模板->绑定数据->计算布局->页面绘制,计算布局结束后,元素在页面上的位置就已经确定了,那么只要拦截这个阶段信息就可以实现视图信息的获取。


通过对于代码的研究,我们发现在`com.sankuai.litho.recycler.AdapterCompat`这个类中控制着视图布局行为,在`bindViewHolder`中完成视图的最终的布局和计算。首先,我们通过在此处插入一个自定义的监听器来拦截布局信息。




    public final void bindViewHolder(BaseViewHolder<Data> viewHolder, int position) {  

            if (viewHolder != null) {  

                viewHolder.bindView(context, getData(position), position);  


                //自动化测试回调  

                if (componentTreeCreateListeners != null) {  

                    if (viewHolder instanceof LithoViewHolder) {  

                        DataHolder holder = getData(position);  

                        //获取视图布局信息  

                        LithoView view = ((LithoViewHolder<Data>) viewHolder).lithoView;  

                        LayoutController layoutController = ((LithoDynamicDataHolder) holder).getLayoutController(null);  

                        VirtualNodeBase node = layoutController.viewNodeRoot;  

                        //通过监听器将视图信息向外传递给可测性SDK  

                        componentTreeCreateListeners.onComponentTreeCreated(node, view.getRootView(), view.getComponentTree());  

                    }  

                }  

            }  

        }


然后,通过暴露一个静态方法给可测性SDK,完成监听器的初始化。




    public static void setComponentTreeCreateListener(ComponentTreeCreateListener l) {  

            AdapterCompat.componentTreeCreateListeners = l;  

            try {  

                // 兼容mbc的动态布局自动化测试,为避免循环依赖,采用反射调用  

                Class<?> mbcDynamicClass = Class.forName("com.sankuai.meituan.mbc.business.item.dynamic.DynamicLithoItem");  

                Method setComponentTreeCreateListener = mbcDynamicClass.getMethod("setComponentTreeCreateListener", ComponentTreeCreateListener.class);  

                setComponentTreeCreateListener.invoke(null, l);  


            } catch (Exception e) {  

                e.printStackTrace();  

            }  


            try {  

                // 搜索新框架动态布局自动化测试  

                Class<?> searchDynamicClass = Class.forName("com.sankuai.meituan.search.result2.model.DynamicItem");  

                Method setSearchComponentTreeCreateListener = searchDynamicClass.getMethod("setComponentTreeCreateListener", ComponentTreeCreateListener.class);  

                setSearchComponentTreeCreateListener.invoke(null, l);  

            } catch (Exception e) {  

                e.printStackTrace();  

            }  

        }


最后,自动化通过设置自定义的监听器来完成视图信息的获取和存储。




    //通过静态方法设置一个ComponentTreeCreateListener来监听布局事件  

    AdapterCompat.setComponentTreeCreateListener(new AdapterCompat.ComponentTreeCreateListener() {  

                @Override  

                public void onComponentTreeCreated(VirtualNodeBase node, View rootView, ComponentTree tree) {  

                    //将信息存储到一个自定义的ViewInfoObserver对象中  

                    ViewInfoObserver vif = new ViewInfoObserver();  

                    vif.update(node, rootView, tree);  

                }  

            });


我们将视图信息存储在ViewInfoObserver这样一个对象中。




    public class ViewInfoObserver implements AutoTestObserver{  

        public static HashMap<String, View> VIEW_MAP = new HashMap<>();  

        public static HashMap<VirtualNodeBase, View> VIEW = new HashMap<>();  

        public static HashMap<String, ComponentTree> COMPTREE_MAP = new HashMap<>();  

        public static String uri = "http://dashboard.ep.dev.sankuai.com/outter/dynamicTemplateKeyFromJson";  


        @Override  

        public void update(VirtualNodeBase vn, View view,ComponentTree tree) {  

            if (null != vn && null != vn.jsonObject) {  

                try {  

                    String string = vn.jsonObject.toString();  

                    Gson g = new GsonBuilder().setPrettyPrinting().create();  

                    JsonParser p = new JsonParser();  

                    JsonElement e = p.parse(string);  


                    String templateName = null;  

                    String name1 = getObject(e,"templateName");  

                    String name2 = getObject(e,"template_name");  

                    String name3 = getObject(e,"template");  

                    templateName = null != name1 ? name1 : (null != name2 ? name2 : (null != name3 ? name3 : null));  


                    if (null != templateName) {  

                    //如果已经存储则更新视图信息  

                        if (VIEW_MAP.containsKey(templateName)) {  

                            VIEW_MAP.remove(templateName);  

                        }  

                        //存储视图编号  

                        VIEW_MAP.put(templateName, view);  

                        if (VIEW.containsKey(templateName)) {  

                            VIEW.remove(templateName);  

                        }  

                        //存储视图信息  

                        VIEW.put(vn, view);  

                        if (COMPTREE_MAP.containsKey(templateName)) {  

                            COMPTREE_MAP.remove(templateName);  

                        }  

                        COMPTREE_MAP.put(templateName, tree);  

                        System.out.println("autotestDyn:update success");  


                    }   


                } catch (Exception e) {  

                    System.out.println(e.toString());  

                    System.out.println("autotestDyn:templateName not exist!");  

                }  

            }  

        }


当需要查询这些信息的时候,就可以通过XrayDumper来完成信息的输出。




    public class SubViewInfo {  

        public JSONObject getOutData(String template) throws JSONException {  

            JSONObject outData = new JSONObject();  

            JSONObject componentTouchables = new JSONObject();  


            if (!COMPTREE_MAP.isEmpty() && COMPTREE_MAP.containsKey(template) && null != COMPTREE_MAP.get(template)) {  

                ComponentTree cpt = COMPTREE_MAP.get(template);  

                JSONArray componentArray = new JSONArray();  


                ArrayList<View> touchables = cpt.getLithoView().getTouchables();  

                LithoView lithoView = cpt.getLithoView();  

                int[] ls = new int[2];  

                lithoView.getLocationOnScreen(ls);  

                int pointX = ls[0];  

                int pointY = ls[1];  


                for (int i = 0; i < touchables.size(); i++) {  

                    JSONObject temp = new JSONObject();  

                    int height = touchables.get(i).getHeight();  

                    int width = touchables.get(i).getWidth();  

                    int[] tl = new int[2];  

                    touchables.get(i).getLocationOnScreen(tl);  

                    temp.put("height",height);  

                    temp.put("width",width);  

                    temp.put("pointX",tl[0]);  

                    temp.put("pointY",tl[1]);  


                    String url = "";  

                    try {  

                        EventHandler eh = (EventHandler) getValue(getValue(touchables.get(i), "mOnClickListener"), "mEventHandler");  

                        DynamicClickListener listener = (DynamicClickListener) getValue(getValue(eh, "mHasEventDispatcher"), "listener");  

                        Uri clickUri = (Uri) getValue(listener, "uri");  

                        if (null != clickUri) {  

                            url = clickUri.toString();  

                        }  

                    } catch (Exception e) {  

                        Log.d("autotest", "get click url error!");  

                    }  


                    temp.put("url",url);  

                    componentArray.put(temp);  

                }  

                componentTouchables.put("componentTouchables",componentArray);  

                componentTouchables.put("componentTouchablesCount", cpt.getLithoView().getTouchables().size());  


                View[] root = (View[])getValue(cpt.getLithoView(),"mChildren");  

                JSONArray allComponentArray = new JSONArray();  

                if (root.length > 0) {  

                    for (int i = 0; i < root.length; i++) {  

                        try {  

                            if (null != root[i]) {  

                                Object items[] = (Object[]) getValue(getValue(root[i], "mMountItems"), "mValues");  

                                componentTouchables.put("componentCount", items.length);  

                                for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {  

                                    getMountItems(allComponentArray, items[itemIndex], pointX, pointY);  

                                }  

                            }  

                        } catch (Exception e) {  


                        }  

                    }  

                }  

                componentTouchables.put("componentUntouchables",allComponentArray);  

            } else {  

                Log.d("autotest","COMPTREE_MAP is null!");  

            }  

            outData.put(template,componentTouchables);  

            System.out.println(outData);  

            return outData;  

        }  

        }  

    }


### 视图信息的输出-XrayServer


我们获取到了信息,接下来就要考虑如何将视图信息传递给自动化测试脚本,我们参考了Appium的设计。


Appium通过在手机上安装的InstrumentsClient启动了一个SocketServer通过HTTP协议来完成自动化和底层测试框架的数据通信。我们也可以借鉴上述思路,在美团App中启动一个WebServer来完成信息的输出。


第一步,我们实现了一个继承了Service组件,这样就可以方便的通过命令行的方式的启动和停止可测性的功能。




    public class AutoTestServer extends Service  {  

        @Override  

        public IBinder onBind(Intent intent) {  

            return null;  

        }  


        @Override  

        public int onStartCommand(Intent intent, int flags, int startId) {  

        ....  

            return super.onStartCommand(intent, flags, startId);  

        }  

    }


第二步,通过HttpServer的方式对外暴露通信的接口。




    public class AutoTestServer extends Service  {  

        @Override  

        public IBinder onBind(Intent intent) {  

            return null;  

        }  


        @Override  

        public int onStartCommand(Intent intent, int flags, int startId) {  

            // 创建对象,端口通过参数传入  

            if (intent != null) {  

                int randNum = intent.getIntExtra("autoTestPort",8999);  

                HttpServer myServer = new HttpServer(randNum);  

                try {  

                    // 开启HTTP服务  

                    myServer.start();  

                    System.out.println("AutoTestPort:" + randNum);  

                } catch (IOException e) {  

                    System.err.println("AutoTestPort:" + e.getMessage());  

                    myServer = new HttpServer(8999);  

                    try {  

                        myServer.start();  

                        System.out.println("AutoTestPort:8999");  

                    } catch (IOException e1) {  

                        System.err.println("Default:" + e.getMessage());  

                    }  

                }  

            }  

            return super.onStartCommand(intent, flags, startId);  

        }  

    }


第三步,将之前设置好的监听器进行注册。




    public class AutoTestServer extends Service  {  

        @Override  

        public IBinder onBind(Intent intent) {  

            return null;  

        }  


        @Override  

        public int onStartCommand(Intent intent, int flags, int startId) {  

        //注册监听器  

            AdapterCompat.setComponentTreeCreateListener(new AdapterCompat.ComponentTreeCreateListener() {  

                @Override  

                public void onComponentTreeCreated(VirtualNodeBase node, View rootView, ComponentTree tree) {  

                    ViewInfoObserver vif = new ViewInfoObserver();  

                    vif.update(node, rootView, tree);  

                }  

            });  


            // 创建对象,端口通过参数传入  

            .....  

            return super.onStartCommand(intent, flags, startId);  

        }  

    }


最后,在HttpServer中通过不同的路径来实现接收不同的指令。




    private JSONObject getResponseByUri(@Nonnull IHTTPSession session) throws JSONException {  

            String uri = session.getUri();  

            if (isFindCommand(uri)) {  

                return getResponseByFindUri(uri);  

            }  

    }  


    @Nonnull  

    private JSONObject getResponseByFindUri(@Nonnull String uri) throws JSONException {  

        String template = uri.split("/")[2];  

        String protocol = uri.split("/")[3];  

        switch (protocol) {  

            case "frame":  

                TemplateLayoutFrame tlf = new TemplateLayoutFrame();  

                return tlf.getOutData(template);  

            case "subview":  

                SubViewInfo svi = new SubViewInfo();  

                return svi.getOutData(template);  

            //省略了部分的代码处理逻辑      

            ....  

            default:  

                JSONObject errorJson = new JSONObject();  

                errorJson.put("success", false);  

                errorJson.put("message", "输入find链接地址有误");  

                return errorJson;  

        }  

    }


### SDK整体功能结构


自动化脚本通过访问设备的特定端口(例如:http://localhost:8899/find/subview),经由XrayServer,通过访问路径将请求转发至XrayDumper进行信息的提取和输出。然后布局解析器将布局信息序列化成JSON数据,再经由XrayServer,通过网络以HTTP响应的方式传到给自动化测试脚本。


图9 XraySDK功能结构示意图


### 视图信息的增强


除了常规的位置、内容、类型等信息,我们还通过检查时间监听器的方式,进一步判断视图元素是否可以进行交互,进一步增强了页面视图结构的有效信息。




    // setGestures  

    ArrayList<String> gestures = new ArrayList<>();  

    if (view.isClickable()){  

       gestures.add("isClickable");  

    }  

    if (view.isLongClickable()){  

       gestures.add("isLongClickable");  

    }  

    //省略部分代码  

    .....


### 动态布局自动化的收益


基于视图可测性的提升,美团动态化卡片的自动化测试覆盖度有了大幅的提升,从原来无法做自动化测试,到目前80%以上的动态化卡片都实现了自动化测试,而且效率也得到了明显的提升。


图10 自动化效率提升收益


## 未来展望


页面视图信息作为客户端测试最基础且重要的属性之一,是对用户视觉信息的一种代码级的表示。它对于机器识别页面元素信息有着非常重要的作用,对于它的可测性改造将会给技术团队带来很大的收益。我们会列举了几个视图可测性改造的探索方向,仅供大家参考。


### 使用视图解析原理解决WebView元素定位


应用同样的思想,我们还可以用来解决WebView元素定位的问题。


图11 WebView页面示例


通过运行在App内部的SDK,可以获取到对应的WebView实例。通过获取到根节点,从根节点开始进行循环遍历,同时把每个节点的信息存储下来就可以得到所有的视图信息了。


在WebView是否也有同样合适的根节点呢?基于对于HTML的理解,我们可以想到HTML中所有的标签都是挂在BODY标签下面的,BODY标签就是我们需要选取的根节点。我们可以通过WebElement["attrName"]的方式来进行属性的获取。


图12 遍历WebView节点的代码示例


### 视图可测性改造更多的应用场景


  *  **提升功能测试可靠性** :在功能测试自动化中,通过内部更加稳定和迅速的视图信息输出,可以有效提升自动化测试的稳定性。避免由于元素无法获取或者元素获取缓慢导致的自动化测试失败。

  *  **提升可靠性测试效率** :对于依靠随机或者按照视图信息进行页面随机操作的可靠性测试,依赖对于视图信息的过滤,也可以只操作可以交互的元素(通过过滤元素事件监听器是否为空)。这样就可以有效提升可靠性测试的效率,在单位时间内可以完成更多页面的检测。

  *  **增加兼容性测试检测手段** :在页面兼容性方面,通过对页面组件位置信息和属性来扫描页面内是否存在不合理的堆叠、空白区域、形状异常等UI呈现异常。也可以获取内容信息,例如图片、文本,来检查是否存在不适宜内容呈现。可以作为图像对比方案的有效补充。


 **招聘信息** 美团平台质量技术中心,负责美团 App

业务和大前端(移动客户端和Web前端)基础技术质量工作,沉淀流程规范和配套工具、提升研发效率。团队技术一流、氛围良好,感兴趣的同学简历可以发送至:

hrbp@ceshiren.com  





来霍格沃兹测试开发学社,学习更多软件测试与测试开发的进阶技术,知识点涵盖web自动化测试 app自动化测试、接口自动化测试、测试框架、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移、测试右移、精准测试、测试平台开发、测试管理等内容,课程技术涵盖bash、pytest、junit、selenium、appium、postman、requests、httprunner、jmeter、jenkins、docker、k8s、elk、sonarqube、jacoco、jvm-sandbox等相关技术,全面提升测试开发工程师的技术实力


视频资料领取:https://qrcode.testing-studio.com/f?from=jianshu&url=https://ceshiren.com/t/topic/15844

点击查看更多信息

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容

  • 前几天整理了Java面试题集合,今天再来整理下Android相关的面试题集合.如果你希望能得到最新的消息,可以关注...
    Boyko阅读 3,617评论 8 135
  • 面试必背 会舍弃、总结概括——根据我这些年面试和看面试题搜集过来的知识点汇总而来 建议根据我的写的面试应对思路中的...
    luoyangzk阅读 6,727评论 6 173
  • 【威哥说】虽然不能说所有的面试题都是必学的技术知识点,但是大家学习都是以找工作为目标,时刻了解用人单位的技术要求,...
    磨砺营阅读 1,849评论 6 92
  • Android面试题 Android面试题包括Android基础,还有一些源码级别的、原理这些等。所以想去大公司面...
    陈二狗想吃肉阅读 4,763评论 0 21
  • 什么是单元测试 在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最...
    HelloCsl阅读 10,932评论 1 46