草庐IT

Android10.0 最近任务Recents功能分析

孤街酒客0911 2023-03-28 原文

在Android10.0上,Recents功能分布在SystemUI和Launcher3里面集成.

一.初始化

跟Android8.1类似,先看初始化入口:

1.Recents.java

Recents继承SystemUI,进程启动后会在Dependency里面通过@Inject进行初始化,然后在SystemUIService里面调用SystemUIApplication的startServicesIfNeeded()里面进行启动:

private final RecentsImplementation mImpl
@Override
public void start() {
    //加入callback
    mCommandQueue.addCallback(this);
    mImpl.onStart(mContext);
}

RecentsImplementation替代了之前的RecentsImpl,是一个interface,由其实现类OverviewProxyRecentsImpl来执行操作,关于该类的实例化用到了dagger2,不懂的同学可以去学习一下;

2.OverviewProxyRecentsImpl.java

private OverviewProxyService mOverviewProxyService;
@Override
public void onStart(Context context) {
   
    mOverviewProxyService = Dependency.get(OverviewProxyService.class);
}

根据调用关系,在onStart()里面创建了OverviewProxyService对象,看一下具体实现:

3.OverviewProxyService.java

中间类,在该类内部去绑定远端的service:

@Inject
public OverviewProxyService(Context context, CommandQueue commandQueue,
            NavigationBarController navBarController, NavigationModeController navModeController,
            NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
            PipUI pipUI, Optional<Divider> dividerOptional,
            Optional<Lazy<StatusBar>> statusBarOptionalLazy,
            BroadcastDispatcher broadcastDispatcher) {
    super(broadcastDispatcher);

    mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
            com.android.internal.R.string.config_recentsComponentName));
    mQuickStepIntent = new Intent(ACTION_QUICKSTEP)
            .setPackage(mRecentsComponentName.getPackageName());

   

    // Connect to the service
    updateEnabledState();
    startConnectionToCurrentUser();
}

public void startConnectionToCurrentUser() {
    if (mHandler.getLooper() != Looper.myLooper()) {
        mHandler.post(mConnectionRunnable);
    } else {
        internalConnectToCurrentUser();
    }
}

private void internalConnectToCurrentUser() {
    
    Intent launcherServiceIntent = new Intent(ACTION_QUICKSTEP)
                .setPackage(mRecentsComponentName.getPackageName());
    try {
        mBound = mContext.bindServiceAsUser(launcherServiceIntent,
                    mOverviewServiceConnection,
                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                    UserHandle.of(getCurrentUserId()));
        }
    
}

private IOverviewProxy mOverviewProxy;
private final ServiceConnection mOverviewServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
      
        mOverviewProxy = IOverviewProxy.Stub.asInterface(service);
       
    }
}

跟随调用关系来看,在构造方法内部,会去执行startConnectionToCurrentUser来调用bindServiceAsUser()去启动service,在onServiceConnected中,通过IOverviewProxy.Stub.asInterface(service)来获取IOverviewProxy实例,后续会用到;
Service对应的Intent为android.intent.action.QUICKSTEP_SERVICE,R.string.config_recentsComponentName对应如下,用来获取远端服务的应用包名:

<string name="config_recentsComponentName" translatable="false"
        >com.android.launcher3/com.android.quickstep.RecentsActivity</string>

从ComponentName可以看到,被bind的service是在Launcher3里面,接下来一起看一下对应的远端Service;

4.TouchInteractionService.java

Launcher3内的入口类,通过该类来调用相关逻辑:

private final IBinder mMyBinder = new IOverviewProxy.Stub() {
   
    @BinderThread
    @Override
    public void onOverviewToggle() {
        mOverviewCommandHelper.onOverviewToggle();
    }
   
}

@Override
public IBinder onBind(Intent intent) {
    return mMyBinder;
}

可以看到,TouchInteractionService里面创建了IOverviewProxy.Stub实例,然后在onBind()返回;
TouchInteractionService是一个Service,在onCreate()里面进行了一些初始化相关的调用:

@Override
public void onCreate() {
    super.onCreate();
   
    mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
   

    sConnected = true;
}

@UiThread
public void onUserUnlocked() {
   
    mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
    mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
                mOverviewComponentObserver);
   
}

可以看到,在TouchInteractionService启动后,会创建OverviewComponentObserver实例和OverviewCommandHelper实例,一起看一下这两个类的实现:

5.OverviewComponentObserver.java

public OverviewComponentObserver(Context context, RecentsAnimationDeviceState deviceState) {
    
    ComponentName fallbackComponent = new ComponentName(mContext, RecentsActivity.class);
    mFallbackIntent = new Intent(Intent.ACTION_MAIN)
                .addCategory(Intent.CATEGORY_DEFAULT)
                .setComponent(fallbackComponent)
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    
    updateOverviewTargets();
}

可以看到,在构造方法内部会创建mFallbackIntent,从组成来看,通过该Intent启动RecentsActivity;接下来看一下 updateOverviewTargets():

private void updateOverviewTargets() {
   
    mActivityInterface = FallbackActivityInterface.INSTANCE;
    mIsHomeAndOverviewSame = false;
    mOverviewIntent = mFallbackIntent;
   
}

在该方法内部,会将mFallbackIntent赋值给mOverviewIntent,后续启动Recents会用到mOverviewIntent;

6.OverviewCommandHelper.java

启动Recents的直接入口类,最终实现了onOverviewToggle():

public OverviewCommandHelper(Context context, RecentsAnimationDeviceState deviceState,
            OverviewComponentObserver observer) {
    
    mRecentsModel = RecentsModel.INSTANCE.get(mContext);
    mOverviewComponentObserver = observer;
}

@BinderThread
public void onOverviewToggle() {
   
    ActivityManagerWrapper.getInstance()
                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
    MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
}

以上就是Recents功能在初始化过程中涉及到的关键类,用一张流程图总结一下:

image

二.启动

前面分析了初始化涉及到的关键类,系统启动后会启动SystemUI进程,然后进行一系列初始化,接下来看一下进入Recents的流程:
关于手势或Key按键触发这一块逻辑处理跟Android8.1处理是一样的,入口都是在PhoneWindowManager,咱们从Recents接收toggleRecentApps()分析:

1.Recents.java

@Override
public void toggleRecentApps() {
   
    mImpl.toggleRecentApps();
}

2.OverviewProxyRecentsImpl.java

@Override
public void toggleRecentApps() {
    // If connected to launcher service, let it handle the toggle logic
    IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
    if (overviewProxy != null) {
        final Runnable toggleRecents = () -> {
            try {
                if (mOverviewProxyService.getProxy() != null) {
                    mOverviewProxyService.getProxy().onOverviewToggle();
                    mOverviewProxyService.notifyToggleRecentApps();
                }
            } catch (RemoteException e) {
                Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
            }
        };
        //启动runnable
    }
}

在runnable内部会通过OverviewProxyService的getProxy()来获取到Launcher3端实现的IOveriewProxy对象引用,然后调用onOverviewToggle()方法:

3.TouchInteractionService.java

@BinderThread
@Override
public void onOverviewToggle() {
    mOverviewCommandHelper.onOverviewToggle();
}

4.OverviewCommandHelper.java

@BinderThread
public void onOverviewToggle() {

   ActivityManagerWrapper.getInstance()
                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
   MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
}

可以看到,在执行onOverviewToggle()后,实际上是通过Executoe执行了RecentsActivityCommand:

private class RecentsActivityCommand<T extends StatefulActivity<?>> implements Runnable {
  
    public RecentsActivityCommand() {
        mActivityInterface = mOverviewComponentObserver.getActivityInterface();
      

        //预加载,Android8.1也有相同的逻辑
        mRecentsModel.getTasks(null);
    }

    @Override
     public void run() {
         
         // Otherwise, start overview.
         mListener = mActivityInterface.createActivityInitListener(this::onActivityReady);
         mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
                    new RemoteAnimationProvider() {
                        @Override
                        public AnimatorSet createWindowAnimation(
                                RemoteAnimationTargetCompat[] appTargets,
                                RemoteAnimationTargetCompat[] wallpaperTargets) {
                            return RecentsActivityCommand.this.createWindowAnimation(appTargets,
                                    wallpaperTargets);
                        }
                    }, mContext, MAIN_EXECUTOR.getHandler(),
                    mAnimationProvider.getRecentsLaunchDuration());
        }

可以看到,最终是通过registerAndStartActivity()来启动了intent,前面分析到mOverviewComponentObserver.getOverviewIntent()对应的就是mFallbackIntent,最终启动了RecentsActivity;
用一张流程图总结一下:

image

三.显示

在启动RecentsActivity后,会显示最近任务列表,看一下具体工作流程:

1.RecentsActivity.java

Recents显示Activity

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  
    setupViews();
   
}

protected void setupViews() {
    inflateRootView(R.layout.fallback_recents_activity);
    setContentView(getRootView());
    mDragLayer = findViewById(R.id.drag_layer);
    mFallbackRecentsView = findViewById(R.id.overview_panel);
    mActionsView = findViewById(R.id.overview_actions_view);

    mDragLayer.recreateControllers();
    mFallbackRecentsView.init(mActionsView);
}

RecentsActivity继承了StatefulActivity,有些方法实现是在父类里面执行的,在onCreate()里面执行setupViews(),初始化了FallbackRecentsView,FallbackRecentsView继承了RecentsView,主要逻辑都是在RecentsView里面实现的,直接看RecentsView的实现逻辑:

2.RecentsView.java

Recents显示主View

public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
            BaseActivityInterface sizeStrategy) {
    super(context, attrs, defStyleAttr);
    
    mModel = RecentsModel.INSTANCE.get(context);

    mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                .inflate(R.layout.overview_clear_all_button, this, false);
    mClearAllButton.setOnClickListener(this::dismissAllTasks);
    mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                10 /* initial size */);

   
}

可以看到,在构造方法内部,获取了RecentsModel实例,创建了ViewPool实例mTaskViewPool,该mTaskViewPool存储TaskView,对应的layout为 R.layout.task,最大数量为20;

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    updateTaskStackListenerState();
    ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
    //当snapshot更新时,会进行回调刷新UI
    RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
}

在RecentsView显示时会回调onAttachedToWindow(),在内部执行了updateTaskStackListenerState(),然后做了一些注册回调操作,当有变化时,会进行回调通知来更新UI;

private void updateTaskStackListenerState() {
    boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow()
                && getWindowVisibility() == VISIBLE;
    if (handleTaskStackChanges != mHandleTaskStackChanges) {
        mHandleTaskStackChanges = handleTaskStackChanges;
        if (handleTaskStackChanges) {
            reloadIfNeeded();
        }
    }
}

在updateTaskStackListenerState()内部会进行一系列条件判断来确定是否执行reloadIfNeeded(),当首次进入时会执行reloadIfNeeded():

public void reloadIfNeeded() {
    if (!mModel.isTaskListValid(mTaskListChangeId)) {
        mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
    }
}

通过RecentsModel的getTasks()来获取任务列表,然后回到applyLoadPlan(),getTasks()逻辑在后面进行分析,先看一下applyLoadPlan()方法的执行逻辑:

protected void applyLoadPlan(ArrayList<Task> tasks) {

    // Unload existing visible task data
    unloadVisibleTaskData();

    final int requiredTaskCount = tasks.size();
    if (getTaskViewCount() != requiredTaskCount) {
        for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
            addView(mTaskViewPool.getView());
        }
        if (requiredTaskCount > 0) {
            addView(mClearAllButton);
        }
    }

    // Rebind and reset all task views
    for (int i = requiredTaskCount - 1; i >= 0; i--) {
        final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
        final Task task = tasks.get(i);
        final TaskView taskView = (TaskView) getChildAt(pageIndex);
        taskView.bind(task, mOrientationState);
    }
    resetTaskVisuals();
    }

在applyLoadPlan()内部,主要执行了四项工作:
  1. unloadVisibleTaskData():将现有visible的task数据进行置空;
  2. 根据task数量(首次进入)进行addView,TaskView通过mTaskViewPool的getView()进行获取,最后添加clearAllButton;
  3. 对添加完的TaskView进行bind()操作,将对应的task存在TaskView内部,类似setTag()功能;
  4. 执行resetTaskVisuals()来刷新加载数据;

public void resetTaskVisuals() {
    // Update the set of visible task's data
    loadVisibleTaskData();
}

public void loadVisibleTaskData() {

    // Update the task data for the in/visible children
    for (int i = 0; i < getTaskViewCount(); i++) {
        TaskView taskView = getTaskViewAt(i);
        Task task = taskView.getTask();
         int index = indexOfChild(taskView);
         boolean visible = lower <= index && index <= upper;
         if (visible) {
             if (task == mTmpRunningTask) {
                // Skip loading if this is the task that we are animating into
                 continue;
             }
             if (!mHasVisibleTaskData.get(task.key.id)) {
                 taskView.onTaskListVisibilityChanged(true /* visible */);
             }
             mHasVisibleTaskData.put(task.key.id, visible);
         }
    }
}

最终在loadVisibleTaskData()里面通过TaskView的onTaskVisibilityChanged(true)来加载数据;

3.TaskView.java

Recents列表中Task对应的显示View

public void bind(Task task, RecentsOrientedState orientedState) {
    mTask = task;
    mSnapshotView.bind(task);
}

public void onTaskListVisibilityChanged(boolean visible) {
    if (mTask == null) {
        return;
    }
        cancelPendingLoadTasks();
    if (visible) {
        // These calls are no-ops if the data is already loaded, try and load the high
        // resolution thumbnail if the state permits
        RecentsModel model = RecentsModel.INSTANCE.get(getContext());
        TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
        TaskIconCache iconCache = model.getIconCache();
        mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(
                    mTask, thumbnail -> mSnapshotView.setThumbnail(mTask, thumbnail));
        mIconLoadRequest = iconCache.updateIconInBackground(mTask,
                    (task) -> {
                        setIcon(task.icon);
                        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
                            getRecentsView().updateLiveTileIcon(task.icon);
                        }
                        mDigitalWellBeingToast.initialize(mTask);
                    });
        } else {
            mSnapshotView.setThumbnail(null, null);
            setIcon(null);
            // Reset the task thumbnail reference as well (it will be fetched from the cache or
            // reloaded next time we need it)
            mTask.thumbnail = null;
        }
}

在onTaskListVisibilityChanged()内部,当visible为true时,执行mSnapshotView.setThumbnail()和setIcon()分别来加载缩略图和icon;当visible为false时,将其置空;

public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setOnClickListener((view) -> {
         if (getTask() == null) {
             return;
         }
         launchTask(true /* animate */);
        });
}

public void launchTask(boolean animate) {
    launchTask(animate, false /* freezeTaskList */);
}

private void launchTaskInternal(boolean animate, boolean freezeTaskList,
            Consumer<Boolean> resultCallback, Handler resultCallbackHandler) {
    if (mTask != null) {
        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
                    opts, resultCallback, resultCallbackHandler);
        getRecentsView().onTaskLaunched(mTask);
    }
}

在TaskView内部设置了点击事件监听,当点击后会执行launchTask,最终会调用到ActivityManagerWrapper的startActivityFromRecentsAsync()来快速切换到对应的任务;

4.RecentsModel.java

Recents数据获取功能管理类

private RecentsModel(Context context) {
    mContext = context;
    mTaskList = new RecentTasksList(MAIN_EXECUTOR,
                new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
    mIconCache = new TaskIconCache(context, looper);
    mThumbnailCache = new TaskThumbnailCache(context, looper);

   ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
}

   RecentsModel继承了TaskStackChangeListener,在构造方法内部初始化了RecentsTaskList、TaskIconCache和TaskThumbnailCache实例,注册了registerTaskStackListener回调;分别来获取最近任务列表、获取Task对应的Icon和,
   RecentsTaskList:获取最近任务列表;
   TaskIconCache:获取Task对应的icon,并进行缓存;
   TaskThumbnailCache:获取Task对应的thumbnailData,并进行缓存;
   与Android8.1不同的是,8.1上在获取最近任务列表后会获取任务对应的Thumbnail和Icon,最终封装成Task,在显示时直接通过Task.thumbnail和Task.icon就可以直接显示;11上会通过TaskIconCache和TaskThumbnailCache进行分别存储管理,首次显示或有新的任务,需要通过TaskIconCache和TaskThumbnailCache执行对应的request去获取并进行cache存储;
public int getTasks(Consumer<ArrayList<Task>> callback) {
    return mTaskList.getTasks(false /* loadKeysOnly */, callback);
}

执行getTasks时,实际是通过RecentsTaskList的getTasks()来执行的;

@Override
public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
    mThumbnailCache.updateTaskSnapShot(taskId, snapshot);

    for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
        Task task = mThumbnailChangeListeners.get(i).onTaskThumbnailChanged(taskId, snapshot);
        if (task != null) {
            task.thumbnail = snapshot;
        }
    }
}

   当Task的snapshot截取完毕后,会收到onTaskSnapshotChanged()回调,先对snapshot进行缓存,然后执行onTaskThumbnailChanged()通知,在RecentsView里面对thumbnail进行更新;

5.RecentsTaskList.java

获取最近任务列表类

public synchronized int getTasks(boolean loadKeysOnly, Consumer<ArrayList<Task>> callback) {

    // Kick off task loading in the background
    UI_HELPER_EXECUTOR.execute(() -> {
       if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
            mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
       }
       TaskLoadResult loadResult = mResultsBg;
       mMainThreadExecutor.execute(() -> {
           mResultsUi = loadResult;
           if (callback != null) {
               ArrayList<Task> result = copyOf(mResultsUi);
               callback.accept(result);
           }
      });
    });

   return requestLoadId;
}

在getTasks()内部通过loadTasksInBackgroud()来获取TaskLoadResult对象mResultsBg,然后在主线程里面进行回调,最终执行到RecentsView里面的applyLoadPlan()是在主线程里面刷新UI;先看一下loadTasksInBackground()方法:

TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
    int currentUserId = Process.myUserHandle().getIdentifier();
    List<ActivityManager.RecentTaskInfo> rawTasks =
                mActivityManagerWrapper.getRecentTasks(numTasks, currentUserId);
    // The raw tasks are given in most-recent to least-recent order, we need to reverse it
    Collections.reverse(rawTasks);


    TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
    for (ActivityManager.RecentTaskInfo rawTask : rawTasks) {
        Task.TaskKey taskKey = new Task.TaskKey(rawTask);
        Task task;
        if (!loadKeysOnly) {
            boolean isLocked = tmpLockedUsers.get(taskKey.userId);
            task = Task.from(taskKey, rawTask, isLocked);
        } else {
            task = new Task(taskKey);
        }
        allTasks.add(task);
    }

    return allTasks;
}

可以看到,在loadTasksInBackgroud()内部,通过ActivityManagerWrapper的getRecentTasks()来获取rawTasks,然后反向排序,最后将其处理添加到allTasks,然后返回结果;

6.ActivityManagerWrapper.java

SystemUI与SystemServer交互类

public List<RecentTaskInfo> getRecentTasks(int numTasks, int userId) {
    try {
       return ActivityTaskManager.getService().getRecentTasks(numTasks,RECENT_IGNORE_UNAVAILABLE, userId).getList();
    }
}

public @NonNull ThumbnailData getTaskThumbnail(int taskId, boolean isLowResolution) {
    ActivityManager.TaskSnapshot snapshot = null;
    try {
        snapshot = ActivityTaskManager.getService().getTaskSnapshot(taskId, isLowResolution);
    } catch (RemoteException e) {
        Log.w(TAG, "Failed to retrieve task snapshot", e);
    }
    if (snapshot != null) {
        return new ThumbnailData(snapshot);
    } else {
        return new ThumbnailData();
    }
}

public boolean startActivityFromRecents(int taskId, ActivityOptions options) {
    try {
        Bundle optsBundle = options == null ? null : options.toBundle();
        ActivityTaskManager.getService().startActivityFromRecents(taskId, optsBundle);
        return true;
    } catch (Exception e) {
        return false;
    }
 }

ActivityManagerWrapper提供了跟systemserver交互的接口,相当于Android8.1中的SystemServicesProxy功能;
  用一张流程图总结显示过程:

image

以上就是对Android10.0 Recents功能的分析,功能实现由之前一个进程拆分到两个进程,其他处理基本上保持一致。

原作者:雷涛赛文

有关Android10.0 最近任务Recents功能分析的更多相关文章

  1. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  2. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  3. ruby-on-rails - Cucumber 是否只是 rspec 的包装器以帮助将测试组织成功能? - 2

    只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您

  4. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  5. ruby-on-rails - Rake 任务仅调用一次时执行两次 - 2

    我写了一个非常简单的rake任务来尝试找到这个问题的根源。namespace:foodotaskbar::environmentdoputs'RUNNING'endend当在控制台中执行rakefoo:bar时,输出为:RUNNINGRUNNING当我执行任何rake任务时会发生这种情况。有没有人遇到过这样的事情?编辑上面的rake任务就是写在那个.rake文件中的所有内容。这是当前正在使用的Rakefile。requireFile.expand_path('../config/application',__FILE__)OurApp::Application.load_tasks这里

  6. 由于 libgmp.10.dylib 的问题,Ruby 2.2.0 无法运行 - 2

    我刚刚安装了带有RVM的Ruby2.2.0,并尝试使用它得到了这个:$rvmuse2.2.0--defaultUsing/Users/brandon/.rvm/gems/ruby-2.2.0dyld:Librarynotloaded:/usr/local/lib/libgmp.10.dylibReferencedfrom:/Users/brandon/.rvm/rubies/ruby-2.2.0/bin/rubyReason:Incompatiblelibraryversion:rubyrequiresversion13.0.0orlater,butlibgmp.10.dylibpro

  7. ruby - 帮助使用 Ruby 中的 "Whenever"gem 来执行 cron 任务 - 2

    我以前没有使用过cron,所以我不能确定我这样做是对的。我想要自动化的任务似乎没有运行。我在终端中执行了这些步骤:sudogeminstall每当切换到应用程序目录无论何时。(这创建了文件schedule.rb)我将此代码添加到schedule.rb:every10.minutesdorunner"User.vote",environment=>"development"endevery:hourdorunner"Digest.rss",:environment=>"development"end我将此代码添加到deploy.rb:after"deploy:symlink","depl

  8. ruby-on-rails - rails 功能测试 - 2

    在Rails自动生成的功能测试(test/functional/products_controller_test.rb)中,我看到以下代码:classProductsControllerTest我的问题是:方法调用products()在哪里/如何定义?products(:one)到底是什么意思?看代码,大概意思是“创建一个产品”,但是它是如何工作的呢?注意我是Ruby/Rails的新手,如果这些是微不足道的问题,我深表歉意。 最佳答案 如果您查看test/fixtures文件夹,您会看到一个products.yml文件。这是在您创建

  9. ruby - 在 rake 任务中运行 capybara - 2

    如何在Rake任务中运行Capybara功能?例如:访问('http://google.com')谢谢! 最佳答案 在任务中尝试这样的事情:require'capybara'require'capybara/dsl'Capybara.current_driver=:seleniumBrowser=Class.new{includeCapybara::DSL}page=Browser.new.pagepage.visit("http://www.google.com")puts(page.html)

  10. ruby - ri 有空文件 – Ubuntu 11.10, Ruby 1.9 - 2

    我正在运行Ubuntu11.10并像这样安装Ruby1.9:$sudoapt-getinstallruby1.9rubygems一切都运行良好,但ri似乎有空文档。ri告诉我文档是空的,我必须安装它们。我执行此操作是因为我读到它会有所帮助:$rdoc--all--ri现在,当我尝试打开任何文档时:$riArrayNothingknownaboutArray我搜索的其他所有内容都是一样的。 最佳答案 这个呢?apt-getinstallri1.8编辑或者试试这个:(非rvm)geminstallrdocrdoc-datardoc-da

随机推荐